From 4c024a24f0de33aebce2244eb502e153b36c274a Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 22 Aug 2026 19:10:10 +0200 Subject: [PATCH] firebase: make the rules template impossible to deploy verbatim database.rules.json carries YOUR_FIREBASE_UID on purpose -- this repo is public and the uid is the one value that must not be in it. But nothing stopped anyone publishing the template as-is, and rules pinned to a uid that matches no account deny every read and write. The symptom is every device failing at once with a bare permission error, which reads like an outage rather than a bad paste. render_rules.sh substitutes the uid from ~/.config/crdt-sync/firebase.json and fails closed on anything unsafe: a surviving placeholder, missing `auth != null`, a missing uid pin (that one matters -- `auth != null` alone lets any stranger who self-registers through the public Web API key read everything), or output that is not JSON. Verified: its output is byte-identical to the deployed ~/.config/crdt-sync/database.rules.json, and the live database still answers an unauthenticated read with 401. All three failure paths exercised. Also corrects the README, which described the in-repo copy as "the deployed rules, with the real uid filled in" -- it never was. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG --- firebase/README.md | 21 ++++++++-- firebase/render_rules.sh | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100755 firebase/render_rules.sh diff --git a/firebase/README.md b/firebase/README.md index 71436d8..bf15ffc 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -34,8 +34,23 @@ through the identitytoolkit REST API to anyone holding the Web API key, and that key ships inside the Android APKs. Pinning the uid means a stranger who registers an account still reads and writes nothing. -To deploy, substitute the real uid (Firebase console → Authentication → Users -→ User UID) and paste into **Realtime Database → Rules → Publish**. +The uid here is a **placeholder on purpose** — this repo is public, and the +uid is the one value that must not appear in it. Publishing this file +verbatim yields rules matching no account, which locks every app out of the +project until someone notices; the failure looks like a permission error +from every device at once. + +So do not hand-substitute. Render it: + +```bash +./render_rules.sh # prints deployable rules, real uid filled in +./render_rules.sh --check # validates without printing the uid +``` + +It reads the uid from `~/.config/crdt-sync/firebase.json` and refuses to emit +anything still containing the placeholder, missing `auth != null`, missing a +uid pin, or not parsing as JSON. Paste its output into **Realtime Database → +Rules → Publish**. Verify the rules are live — an unauthenticated request must be refused: @@ -53,7 +68,7 @@ outside version control: |---|---| | `firebase.json` | `apiKey`, `databaseUrl`, `projectId`, `uid`, `email` | | `password` | the sync account's password, no trailing newline | -| `database.rules.json` | the deployed rules, with the real uid filled in | +| `database.rules.json` | the rendered rules, real uid filled in (see `render_rules.sh`) | The Web API key is **not** a secret — it is a public project identifier that ships inside client apps. The password and refresh tokens are the real diff --git a/firebase/render_rules.sh b/firebase/render_rules.sh new file mode 100755 index 0000000..0b6d8f3 --- /dev/null +++ b/firebase/render_rules.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# ============================================================================ +# Render deployable Realtime Database rules from the public template. +# +# The template in this repo carries a placeholder on purpose: the repository +# is public and the uid is the one value that must not appear in it. That +# leaves a trap -- publishing the template verbatim yields rules that match +# no account, which locks every app out of the shared project until someone +# notices. This script closes it: it substitutes the real uid from the local +# config and refuses to emit anything still containing a placeholder. +# ============================================================================ + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_NAME +readonly SCRIPT_DIR +readonly TEMPLATE="$SCRIPT_DIR/database.rules.json" +readonly CONFIG_DIR="${CRDT_SYNC_CONFIG_DIR:-$HOME/.config/crdt-sync}" +readonly PLACEHOLDER="YOUR_FIREBASE_UID" + +usage() { + echo "Usage: $SCRIPT_NAME [--check]" + echo " (no args) print deployable rules with the real uid substituted" + echo " --check verify the rendered rules are sane; print nothing" + exit 0 +} + +# The uid lives in firebase.json, the single source of truth for this machine. +read_uid() { + local config="$CONFIG_DIR/firebase.json" + if [[ ! -r "$config" ]]; then + echo "Error: $config is missing or unreadable." >&2 + echo "It holds the uid these rules pin; see README.md." >&2 + exit 1 + fi + python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['uid'])" \ + "$config" +} + +render() { + local uid="$1" + sed "s/$PLACEHOLDER/$uid/g" "$TEMPLATE" +} + +# Fail closed: a rules file that still names the placeholder, or that any +# unauthenticated request would satisfy, must never reach the console. +validate() { + local rendered="$1" + if [[ "$rendered" == *"$PLACEHOLDER"* ]]; then + echo "Error: rendered rules still contain $PLACEHOLDER." >&2 + exit 1 + fi + if ! grep -q 'auth != null' <<< "$rendered"; then + echo "Error: rendered rules do not require authentication." >&2 + exit 1 + fi + if ! grep -q "auth.uid ===" <<< "$rendered"; then + echo "Error: rendered rules do not pin a uid; 'auth != null' alone" >&2 + echo "lets any stranger who self-registers read and write." >&2 + exit 1 + fi + python3 -c "import json,sys; json.loads(sys.stdin.read())" <<< "$rendered" +} + +main() { + local check_only=false + [[ "${1:-}" == "--check" ]] && check_only=true + [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage + + local uid rendered + uid="$(read_uid)" + rendered="$(render "$uid")" + validate "$rendered" + + if [[ "$check_only" == true ]]; then + echo "Rules render cleanly and pin a uid." >&2 + else + printf '%s\n' "$rendered" + fi +} + +main "$@"