Download index.html from getreadystack/firestore-rules-guard: direct link, hf CLI and curl.
- Browser
- Download file 44.5 kB
-
https://huggingface.co/spaces/getreadystack/firestore-rules-guard/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/firestore-rules-guard/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/firestore-rules-guard/resolve/main/index.html
44.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Firestore Rules checker — check rules before you deploy</title> | |
| <meta name="description" content="This browser tool audits Firestore and Storage rules for public access, expired test mode dates, and unbounded uploads."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/firestore-rules-guard"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Firestore Rules checker — check rules before you deploy</h1> | |
| <p class="lede">Audits firestore.rules and storage.rules for public access, expired test-mode dates and unbounded uploads before you deploy. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">// firestore.rules — drafted by a coding assistant, shipped unchanged | |
| service cloud.firestore { | |
| match /databases/{database}/documents { | |
| match /{document=**} { | |
| allow read, write: if request.time < timestamp.date(2026, 8, 20); | |
| } | |
| match /users/{userId} { | |
| allow read: if request.auth != null; | |
| allow write: if request.auth != null; | |
| } | |
| match /config/public { | |
| allow read, write: if true; | |
| } | |
| match /admin/{docId} { | |
| allow read, write: if request.auth.uid == 'Yk3QpZ7mTnUaB2cE'; | |
| } | |
| } | |
| }</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Audit the .rules file you have open and get every open-access finding with its line number. <strong>In the editor:</strong> Audit every .rules file in the workspace at once and keep a dated Markdown evidence report you can attach to a review. — that part asks for a licence key.<br><a href="https://getreadystack.com/api/buy/cl/polar_cl_g3Qr7cMfa08KRBcujOSVMVHGnBL70yNLs0MuA0ijHo7">Workspace audit + dated Markdown evidence report — $29 once</a><br><small>$29 once · one licence key per person or team seat ·</small></div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var FRG_RULES = [ | |
| {"id": "rules_version_missing", "sev": "warn", "title": "No rules_version = '2'", "msg": "This file has no rules_version = '2' line, so Firebase evaluates it with version 1 semantics and the recursive wildcard behaves differently than the code you read."}, | |
| {"id": "public_access", "sev": "error", "title": "allow ... : if true", "msg": "This path is open to anyone on the internet with your project ID — no sign-in, no rate limit, no audit trail."}, | |
| {"id": "testmode_open", "sev": "error", "title": "Test-mode date still in the future", "msg": "Test mode: every document under this path is world readable and writable until the date in the rule."}, | |
| {"id": "testmode_expired", "sev": "error", "title": "Test-mode date already passed", "msg": "Test mode expired: every client read and write under this path is being denied, so the app is failing for real users."}, | |
| {"id": "recursive_wildcard_write", "sev": "error", "title": "Write on /{document=**}", "msg": "A write grant on the recursive wildcard applies to every collection in the database, including ones added after this file was written."}, | |
| {"id": "auth_any_user_wildcard", "sev": "error", "title": "request.auth != null on a wildcard", "msg": "Any signed-in account reaches this whole subtree — anonymous auth and self-serve sign-up both count as signed in."}, | |
| {"id": "owner_check_missing", "sev": "error", "title": "Owner segment never compared to request.auth.uid", "msg": "The path captures a user id but the condition never compares it to request.auth.uid, so one customer can write another customer's document."}, | |
| {"id": "hardcoded_uid", "sev": "warn", "title": "Hard-coded uid in a rule", "msg": "A literal uid in the rules file is a permanent backdoor that survives role changes and is readable by anyone who downloads your rules."}, | |
| {"id": "delete_unrestricted", "sev": "warn", "title": "Delete granted to any signed-in user", "msg": "Delete is granted on sign-in alone, so a single script can empty this collection and Firestore keeps no client-side undo."}, | |
| {"id": "list_exposure", "sev": "info", "title": "read grants list as well as get", "msg": "read covers both get and list — a client can page the entire collection. Split into get and list if only single-document reads are intended."}, | |
| {"id": "write_no_validation", "sev": "info", "title": "Write with no field validation", "msg": "The condition checks who is writing but never what is written, so any client can add fields or change a price, role or balance."}, | |
| {"id": "storage_size_unbounded", "sev": "warn", "title": "Storage write with no size cap", "msg": "No request.resource.size check on a Storage write: one upload loop bills your project for as much egress and storage as it likes."}, | |
| {"id": "storage_content_type", "sev": "warn", "title": "Storage write with no content-type check", "msg": "No request.resource.contentType check: a path meant for images will accept executables and HTML served from your own bucket domain."} | |
| ] | |
| ;</script> | |
| <script>// Firestore Rules Guard — one engine, used by the VS Code extension and by the free web page. | |
| (function () { | |
| var isNode = (typeof module !== 'undefined' && module.exports); | |
| var RULES = isNode ? require('./rules.json') : window.FRG_RULES; | |
| var BY_ID = {}; | |
| for (var i = 0; i < RULES.length; i++) BY_ID[RULES[i].id] = RULES[i]; | |
| function days(a, b) { return Math.round((a - b) / 86400000); } | |
| function parseToday(s) { | |
| var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(s || '')); | |
| if (!m) return new Date(); | |
| return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])); | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var today = parseToday(opts.today); | |
| var lines = String(text == null ? '' : text).split(/\r?\n/); | |
| var src = lines.join('\n'); | |
| var findings = []; | |
| var isStorage = /service\s+firebase\.storage/.test(src); | |
| function add(id, line, extra, sev) { | |
| var r = BY_ID[id]; | |
| if (!r) return; | |
| findings.push({ check: id, sev: sev || r.sev, msg: extra ? r.msg + ' ' + extra : r.msg, line: line }); | |
| } | |
| if (/service\s+(cloud\.firestore|firebase\.storage)/.test(src) && | |
| !/rules_version\s*=\s*['"]2['"]/.test(src)) add('rules_version_missing', 1); | |
| var stack = []; // { path, depth } | |
| var depth = 0; | |
| // a condition may be wrapped over several lines — join until the statement closes | |
| var units = [], buf = '', start = 0; | |
| for (var u = 0; u < lines.length; u++) { | |
| var t = lines[u].replace(/\/\/.*$/, '').trim(); | |
| if (!buf) { if (!t) continue; start = u + 1; buf = t; } | |
| else { buf += ' ' + t; } | |
| if (/[;{}]$/.test(buf) || !t) { units.push({ text: buf, line: start }); buf = ''; } | |
| } | |
| if (buf) units.push({ text: buf, line: start }); | |
| for (var n = 0; n < units.length; n++) { | |
| var line = units[n].text; | |
| var lineNo = units[n].line; | |
| var mm = /match\s+(\S+)\s*\{/.exec(line); | |
| var scrub = line.replace(/\{\s*\w+\s*(=\s*\*\*)?\s*\}/g, 'VAR'); | |
| var opens = (scrub.match(/\{/g) || []).length; | |
| var closes = (scrub.match(/\}/g) || []).length; | |
| if (mm) stack.push({ path: mm[1], depth: depth + 1 }); | |
| depth += opens - closes; | |
| while (stack.length && stack[stack.length - 1].depth > depth) stack.pop(); | |
| var am = /allow\s+([a-z,\s]+?)\s*:\s*if\s+([^;]*);?/.exec(line); | |
| if (!am) continue; | |
| var verbs = am[1].split(',').map(function (v) { return v.trim(); }).filter(Boolean); | |
| var cond = am[2].trim(); | |
| var path = stack.map(function (s) { return s.path; }).join(''); | |
| var writes = verbs.some(function (v) { return v === 'write' || v === 'create' || v === 'update'; }); | |
| var reads = verbs.some(function (v) { return v === 'read' || v === 'list'; }); | |
| var deletes = verbs.indexOf('delete') >= 0 || verbs.indexOf('write') >= 0; | |
| var recursive = /\{\w+=\*\*\}/.test(path); | |
| var where = 'Path: ' + (path || '(root)') + '.'; | |
| if (/^true$/.test(cond)) { | |
| add('public_access', lineNo, where, writes ? 'error' : 'warn'); | |
| continue; | |
| } | |
| var tm = /request\.time\s*<\s*timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/.exec(cond); | |
| if (tm) { | |
| var when = new Date(Date.UTC(+tm[1], +tm[2] - 1, +tm[3])); | |
| var iso = tm[1] + '-' + ('0' + tm[2]).slice(-2) + '-' + ('0' + tm[3]).slice(-2); | |
| var d = days(when, today); | |
| if (d >= 0) add('testmode_open', lineNo, 'Open until ' + iso + ' (' + d + ' days from today). ' + where); | |
| else add('testmode_expired', lineNo, 'Denied since ' + iso + ' (' + (-d) + ' days ago). ' + where); | |
| continue; | |
| } | |
| if (/^false$/.test(cond)) continue; | |
| var signedIn = /request\.auth\s*!=\s*null|request\.auth\s*\.\s*uid\s*!=\s*null|request\.auth\s*!=\s*(null)/.test(cond); | |
| var ownerCheck = /request\.auth\.uid\s*==|==\s*request\.auth\.uid/.test(cond); | |
| var literalUid = /request\.auth\.uid\s*==\s*['"][^'"]+['"]|['"][^'"]+['"]\s*==\s*request\.auth\.uid/.test(cond); | |
| var ownerSeg = /\{(user|owner|member|account|uid|profile)\w*\}/i.test(path); | |
| if (writes && recursive) add('recursive_wildcard_write', lineNo, where); | |
| if (recursive && signedIn && !ownerCheck) add('auth_any_user_wildcard', lineNo, where); | |
| if (ownerSeg && writes && !ownerCheck && !recursive) add('owner_check_missing', lineNo, where); | |
| if (literalUid) add('hardcoded_uid', lineNo, where); | |
| if (deletes && signedIn && !ownerCheck && !recursive) add('delete_unrestricted', lineNo, where); | |
| if (verbs.indexOf('read') >= 0 && /\{\w+(=\*\*)?\}\s*$/.test(path) && !recursive) | |
| add('list_exposure', lineNo, where); | |
| if (writes && !recursive && !/request\.resource\.data/.test(cond) && !isStorage) | |
| add('write_no_validation', lineNo, where); | |
| if (isStorage && writes && !/request\.resource\.size/.test(cond)) | |
| add('storage_size_unbounded', lineNo, where); | |
| if (isStorage && writes && !/request\.resource\.contentType/.test(cond)) | |
| add('storage_content_type', lineNo, where); | |
| if (!reads && !writes && !deletes) continue; | |
| } | |
| findings.sort(function (a, b) { return a.line - b.line; }); | |
| return { findings: findings, rule_count: RULES.length }; | |
| } | |
| var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length }; | |
| if (isNode) module.exports = API; | |
| if (typeof window !== 'undefined') window.FRGENGINE = API; | |
| })(); | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var FRGREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = FRGREPORT; | |
| else window.FRGREPORT = FRGREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.FRGENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.FRGREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.firestore-rules-guard" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firestore-rules-guard',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/firestore-rules-guard" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firestore-rules-guard',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/firestore-rules-guard" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firestore-rules-guard',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/firestore-rules-guard" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firestore-rules-guard',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/firestore-rules-guard" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firestore-rules-guard',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.firestore-rules-guard | |
| npx @readystack/firestore-rules-guard <file> | |
| npx @readystack/firestore-rules-guard --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 13 rules Firestore Rules Guard checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"firestore-rules-guard",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Audits firestore.rules and storage.rules for public access, expired test-mode dates and missing owner checks — 13 rules, line by line, before you deploy.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance Firebase developer is quoted at $120 an hour, and a manual pass over one project's rules takes about an hour, repeated every time the rules change.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_g3Qr7cMfa08KRBcujOSVMVHGnBL70yNLs0MuA0ijHo7" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script> | |
| <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku63727_result_card.jpg" alt="Firestore Rules Guard" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>10 findings on 6 lines of a 22-line firestore.rules file: that is what comes back when a solo Firebase developer runs an audit over the ruleset a coding assistant wrote for them last month. Three of the ten are errors. One of them means the app is already broken for real users, and nobody has noticed yet.</p> | |
| <p>Firebase security rules are the only thing between a public endpoint and your users' documents. There is no server in front of Firestore to check a session; the rules file is the check. It is also the file most likely to be generated, skimmed and forgotten, because it is short, it looks declarative, and a wrong line fails open instead of failing loudly. <code>firebase deploy --only firestore:rules</code> will publish a ruleset that lets anyone with your project ID page your entire users collection, and print success.</p> | |
| <h3>The line that expires</h3> | |
| <p>Firebase's test-mode template is written to expire 30 days after you create the database:</p> | |
| <p>match /{document=**} { allow read, write: if request.time < timestamp.date(2026, 8, 20); }</p> | |
| <p>Before that date, every document in the project is world readable and world writable. After it, the same line denies every client read and write, and the failure arrives as a broken app rather than as a warning in a terminal. Both states are findings, and not the same one, so the audit takes a date. Run the sample ruleset as of 2026-09-14 and it reports that client access has been denied since 2026-08-20. Run the same text as of 2026-08-01 and it reports the opposite: the database is open to the internet for another 19 days.</p> | |
| <h3>The four lines nobody re-reads</h3> | |
| <p>The rest of the sample is ordinary generated Firebase code, and each line is wrong in a way that reads as right:</p> | |
| <ul><li><code>allow read: if request.auth != null;</code> on <code>/users/{userId}</code>. Any signed-in account reads every user document. Anonymous auth counts as signed in, and so does self-serve sign-up.</li><li><code>allow write: if request.auth != null;</code> on the same path. The path captures a user id and the condition never compares it to <code>request.auth.uid</code>, so one customer writes another customer's document. The condition also checks who is writing but never what is written, so a client can set its own <code>role</code> field.</li><li><code>allow read, write: if true;</code> on a config document. Open to anyone who knows the project ID.</li><li><code>allow read, write: if request.auth.uid == 'Yk3QpZ7mTnUaB2cE';</code>. A literal uid is a permanent backdoor that survives every role change, and rules files ship inside the repository.</li></ul> | |
| <p>Add the missing <code>rules_version = '2';</code> on line 1, which quietly changes how the recursive wildcard matches, and that is 10 findings across 6 lines.</p> | |
| <h3>Why the emulator does not catch this</h3> | |
| <p>The Firebase emulator and the Rules Playground answer the cases you write. They will confirm that a request you imagined passes or fails. They will never volunteer that <code>read</code> also grants <code>list</code>, that a date passed last month, or that a path variable is never compared to the caller. An audit that reads the file itself surfaces the cases you did not think to write, which are precisely the ones a generated ruleset contains.</p> | |
| <h3>What it costs either way</h3> | |
| <p>Freelance Firebase developers are quoted at $120 an hour, and a manual pass over one project's rules takes roughly an hour, repeated every time the rules change. Firestore Rules Guard runs 13 rules over the file in under a second. The corrected version of the same ruleset returns 0 findings.</p> | |
| <p>The audit of the file you have open is free, in the editor and in the browser, with no key and no account: https://getreadystack.com/tools/firestore-rules-guard</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku63727_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku63727_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Firestore Rules Guard actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the firestore.rules or storage.rules file open in your editor and reports every line that grants more access than it appears to grant. Thirteen rules cover public grants, test-mode dates, recursive wildcards, missing owner checks, unvalidated writes and uncapped Storage uploads. Each finding carries a line number and the runtime effect of that line.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Solo developers and small product teams shipping a Firebase app, especially anyone whose firestore.rules file was drafted by a coding assistant and never reviewed line by line. It is also for the developer answering a customer security questionnaire who needs to show that the ruleset was checked and dated before deploy.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is the Firebase emulator not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The emulator and the Rules Playground answer the cases you write. They tell you whether a request you imagined passes; they never tell you that read also grants list, that a test-mode date expired last month, or that a userId path segment is never compared to request.auth.uid. This audit reads the file itself, so unwritten cases still surface.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free is complete for one file: open a .rules file, run the audit, get every finding with its line number, locally, with no key or account. The licence key changes scope and ownership only, never depth: it audits every .rules file in the workspace at once and writes a dated Markdown evidence report you keep.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Freelance Firebase developers are quoted at $120 an hour, and a manual pass over one project's rules takes roughly an hour, repeated every time the rules change. The extension is $29 once, with one licence key per person or team seat and and it reruns in under a second.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Firestore Security Rules Lint - open rules and missing auth checks the file you open against 13 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Firestore Rules Guard actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the firestore.rules or storage.rules file open in your editor and reports every line that grants more access than it appears to grant. Thirteen rules cover public grants, test-mode dates, recursive wildcards, missing owner checks, unvalidated writes and uncapped Storage uploads. Each finding carries a line number and the runtime effect of that line."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Solo developers and small product teams shipping a Firebase app, especially anyone whose firestore.rules file was drafted by a coding assistant and never reviewed line by line. It is also for the developer answering a customer security questionnaire who needs to show that the ruleset was checked and dated before deploy."}}, {"@type": "Question", "name": "Why is the Firebase emulator not enough?", "acceptedAnswer": {"@type": "Answer", "text": "The emulator and the Rules Playground answer the cases you write. They tell you whether a request you imagined passes; they never tell you that read also grants list, that a test-mode date expired last month, or that a userId path segment is never compared to request.auth.uid. This audit reads the file itself, so unwritten cases still surface."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free is complete for one file: open a .rules file, run the audit, get every finding with its line number, locally, with no key or account. The licence key changes scope and ownership only, never depth: it audits every .rules file in the workspace at once and writes a dated Markdown evidence report you keep."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "Freelance Firebase developers are quoted at $120 an hour, and a manual pass over one project's rules takes roughly an hour, repeated every time the rules change. The extension is $29 once, with one licence key per person or team seat and and it reruns in under a second."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Firestore Security Rules Lint - open rules and missing auth checks the file you open against 13 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:63727,slug:"firestore-rules-guard",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/firebase-deploy-leak-audit"><span class="yj-rel-t">Firebase Deploy Leak Audit</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/content-calendar-template"><span class="yj-rel-t">Content Calendar Template with Capacity & Cost Calculator for Small Content Te</span><span class="yj-rel-p">$90</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/qof-90-test-penalty-8996"><span class="yj-rel-t">Form 8996 90% Asset Test and Penalty Checker (2026 testing dates)</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |