getreadystack's picture
Upload index.html with huggingface_hub
e79be27 verified
Raw History Blame Contribute Delete
46.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Firebase Deploy security check — check your deploy config free</title>
<meta name="description" content="This free browser tool audits firebase.json for ignore-list gaps, catch-all public roots, and missing response headers before deployment.">
<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/firebase-deploy-leak-audit"></head>
<body>
<div class="wrap">
<h1>Firebase Deploy security check — check your deploy config free</h1>
<p class="lede">Audits firebase.json before you run firebase deploy: ignore lists that let .env, source maps and admin keys reach the public bundle, catch-all public roots, and missing response headers. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">{
"hosting": {
"public": ".",
"ignore": ["node_modules"],
"rewrites": [{ "source": "**", "destination": "/index.html" }],
"headers": [
{
"source": "**/*.@(js|css)",
"headers": [{ "key": "Cache-Control", "value": "max-age=31536000, immutable" }]
},
{
"source": "**/*.html",
"headers": [{ "key": "Cache-Control", "value": "max-age=31536000" }]
},
{
"source": "**",
"headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }]
}
]
},
"firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" },
"emulators": { "hosting": { "port": 5000 } }
}</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 firebase.json you have open and name every file your next firebase deploy would publish that should never be public.. <strong>In the editor:</strong> Audit every firebase.json in a monorepo in one run and export the findings as a Markdown or JSON report you can commit, attach to a release, or hand to a client. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var FDL_RULES = [
{"id":"json_parse","sev":"high","title":"firebase.json is not valid JSON","msg":"firebase.json does not parse as JSON, so the CLI cannot read it and nothing below could be checked. Fix the syntax first.","fix":"Run `firebase deploy --only hosting --dry-run` and fix the reported position."},
{"id":"public_is_repo_root","sev":"high","title":"public root is the whole repository","msg":"hosting.public points at the repository root, so every file the ignore list misses ships to the public CDN — including .git history, README drafts and local scripts.","fix":"Point hosting.public at your build output directory (dist, build, public)."},
{"id":"public_missing","sev":"med","title":"no public directory declared","msg":"This hosting block declares neither `public` nor `source`, so `firebase deploy` will refuse the target or fall back to a directory you did not choose.","fix":"Add \"public\": \"\u003cyour build output\u003e\" to the hosting block."},
{"id":"ignore_absent","sev":"med","title":"no ignore list — defaults only","msg":"With no ignore list Firebase applies its three defaults (firebase.json, **/.*, **/node_modules/**). Source maps, service-account JSON and backup files inside the public directory are still uploaded.","fix":"Add an explicit ignore array that repeats the three defaults and adds **/*.map and key files."},
{"id":"missing_dotfiles","sev":"high","title":"ignore list drops the dotfile default","msg":"Supplying hosting.ignore REPLACES Firebase's defaults, and this list has no **/.* entry. Every dotfile under the public root — .env, .env.production, .npmrc, .git — is uploaded and served.","fix":"Add \"**/.*\" back to the ignore array."},
{"id":"env_not_ignored","sev":"high","title":".env files are not excluded","msg":"Nothing in the ignore list excludes .env files, so a .env or .env.production sitting in the build output is fetchable at https://\u003csite\u003e/.env by anyone who guesses the path.","fix":"Add \"**/.env*\" (and keep \"**/.*\") to hosting.ignore."},
{"id":"sourcemap_not_ignored","sev":"high","title":"source maps ship to production","msg":"No ignore entry matches *.map, so the bundler's .map files publish next to the minified code and hand any visitor your unminified source, comments and internal paths.","fix":"Add \"**/*.map\" to hosting.ignore, or turn sourcemaps off in the production build."},
{"id":"service_account_not_ignored","sev":"high","title":"admin key files are not excluded","msg":"No ignore entry matches a service-account key (serviceAccount*.json, *adminsdk*.json, *.pem). One of those left in the build folder is a full Firebase Admin credential served over HTTPS.","fix":"Add \"**/*serviceAccount*.json\", \"**/*adminsdk*.json\" and \"**/*.pem\" to hosting.ignore."},
{"id":"missing_node_modules","sev":"med","title":"node_modules is not fully excluded","msg":"The ignore list has no \"**/node_modules/**\" entry, so nested dependency folders under the public root are uploaded file by file — a slow deploy that also publishes package sources.","fix":"Add \"**/node_modules/**\" to hosting.ignore."},
{"id":"missing_firebase_json","sev":"low","title":"firebase.json itself is not ignored","msg":"\"firebase.json\" is a Firebase default that your custom ignore list dropped, so this config — target names, rewrite map, header policy — is served from your own domain.","fix":"Add \"firebase.json\" to the ignore array."},
{"id":"headers_absent","sev":"high","title":"no response headers configured","msg":"This hosting block sets no headers at all, so the site answers with Firebase defaults: no Content-Security-Policy, no HSTS, no nosniff and no framing rule.","fix":"Add a headers entry with source \"**\" and the four baseline security headers."},
{"id":"csp_missing","sev":"high","title":"no Content-Security-Policy header","msg":"No headers rule sets Content-Security-Policy, so any injected or third-party script on the page runs with full privileges and can read the signed-in user's Firebase ID token.","fix":"Add a Content-Security-Policy header on the \"**\" source, starting from default-src 'self'."},
{"id":"hsts_missing","sev":"med","title":"no Strict-Transport-Security header","msg":"Without Strict-Transport-Security a first visit over http:// can be intercepted before the redirect to https:// completes.","fix":"Add \"Strict-Transport-Security\": \"max-age=31536000; includeSubDomains\"."},
{"id":"nosniff_missing","sev":"med","title":"no X-Content-Type-Options header","msg":"Without X-Content-Type-Options: nosniff, a file a user uploaded and you serve back can be sniffed into an executable content type by the browser.","fix":"Add \"X-Content-Type-Options\": \"nosniff\"."},
{"id":"frame_missing","sev":"med","title":"page can be framed by any site","msg":"Neither X-Frame-Options nor a CSP frame-ancestors directive is set, so your signed-in app can be loaded in an invisible iframe on someone else's domain.","fix":"Add \"X-Frame-Options\": \"DENY\" or frame-ancestors 'none' in the CSP."},
{"id":"cors_wildcard","sev":"high","title":"Access-Control-Allow-Origin is *","msg":"A headers rule returns Access-Control-Allow-Origin: * on a broad source, so every other origin can read the responses this site serves, including any JSON the app fetches from its own path.","fix":"Scope the header to the exact asset paths that need it, or name the allowed origin."},
{"id":"html_immutable_cache","sev":"med","title":"HTML cached long-term","msg":"An HTML source is served with a long max-age, so browsers and the CDN keep serving the previous index.html after a deploy and users load old asset hashes that no longer exist.","fix":"Serve HTML with \"Cache-Control\": \"max-age=0, must-revalidate\" and keep long caching for hashed assets only."}
]
;</script>
<script>// Firebase Deploy Leak Audit — one brain, used by the VS Code extension and by the free web page.
const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.FDL_RULES;
const BY_ID = {};
RULES.forEach(function (r) { BY_ID[r.id] = r; });
function lineOf(text, needle) {
const lines = String(text).split(/\r?\n/);
for (let i = 0; i < lines.length; i++) if (lines[i].indexOf(needle) !== -1) return i + 1;
return 1;
}
function hit(out, text, id, where, extra) {
const r = BY_ID[id];
if (!r) return;
out.push({
check: id,
sev: r.sev,
msg: (extra ? extra + ' ' : '') + r.msg + ' Fix: ' + r.fix,
line: lineOf(text, where || '"hosting"')
});
}
function asArray(v) { return Array.isArray(v) ? v : (v ? [v] : []); }
// every header key/value pair declared anywhere in this hosting block
function headerPairs(h) {
const pairs = [];
asArray(h.headers).forEach(function (entry) {
const src = entry && entry.source ? String(entry.source) : '';
asArray(entry && entry.headers).forEach(function (p) {
if (!p || !p.key) return;
pairs.push({ source: src, key: String(p.key).toLowerCase(), value: String(p.value == null ? '' : p.value) });
});
});
return pairs;
}
function hasHeader(pairs, key) {
return pairs.some(function (p) { return p.key === key; });
}
function check(text, opts) {
opts = opts || {};
const findings = [];
let doc;
try {
doc = JSON.parse(String(text));
} catch (e) {
hit(findings, text, 'json_parse', '{', '(' + e.message + ')');
return { findings: findings };
}
const blocks = asArray(doc && doc.hosting);
if (!blocks.length) return { findings: findings };
blocks.forEach(function (h, i) {
if (!h || typeof h !== 'object') return;
const label = blocks.length > 1 ? '[hosting #' + (i + 1) + (h.target ? ' target ' + h.target : '') + ']' : '';
const anchor = h.public !== undefined ? '"public"' : '"hosting"';
// --- where the bundle comes from -------------------------------------
const pub = h.public === undefined ? null : String(h.public).trim();
if (pub !== null && (pub === '.' || pub === './' || pub === '' || pub === '/')) {
hit(findings, text, 'public_is_repo_root', '"public"', label);
}
if (pub === null && h.source === undefined) {
hit(findings, text, 'public_missing', anchor, label);
}
// --- what the ignore list lets through -------------------------------
const ignore = Array.isArray(h.ignore) ? h.ignore.map(function (s) { return String(s).toLowerCase(); }) : null;
const covers = function (frag) { return ignore !== null && ignore.some(function (e) { return e.indexOf(frag) !== -1; }); };
const dotfiles = covers('**/.*');
if (ignore === null) {
hit(findings, text, 'ignore_absent', anchor, label);
} else {
if (!dotfiles) hit(findings, text, 'missing_dotfiles', '"ignore"', label);
if (!covers('node_modules/**')) hit(findings, text, 'missing_node_modules', '"ignore"', label);
if (!ignore.some(function (e) { return e === 'firebase.json' || e.indexOf('firebase*.json') !== -1; })) {
hit(findings, text, 'missing_firebase_json', '"ignore"', label);
}
}
if (!covers('.env') && !dotfiles) hit(findings, text, 'env_not_ignored', ignore === null ? anchor : '"ignore"', label);
if (!covers('.map')) hit(findings, text, 'sourcemap_not_ignored', ignore === null ? anchor : '"ignore"', label);
if (!covers('serviceaccount') && !covers('adminsdk') && !covers('.pem') && !covers('-key.json')) {
hit(findings, text, 'service_account_not_ignored', ignore === null ? anchor : '"ignore"', label);
}
// --- what the CDN answers with ---------------------------------------
const pairs = headerPairs(h);
if (!pairs.length) {
hit(findings, text, 'headers_absent', anchor, label);
} else {
const csp = pairs.filter(function (p) { return p.key === 'content-security-policy'; });
if (!csp.length) hit(findings, text, 'csp_missing', '"headers"', label);
if (!hasHeader(pairs, 'strict-transport-security')) hit(findings, text, 'hsts_missing', '"headers"', label);
if (!hasHeader(pairs, 'x-content-type-options')) hit(findings, text, 'nosniff_missing', '"headers"', label);
const framed = hasHeader(pairs, 'x-frame-options') ||
csp.some(function (p) { return p.value.toLowerCase().indexOf('frame-ancestors') !== -1; });
if (!framed) hit(findings, text, 'frame_missing', '"headers"', label);
}
pairs.forEach(function (p) {
if (p.key === 'access-control-allow-origin' && p.value.trim() === '*') {
hit(findings, text, 'cors_wildcard', 'Access-Control-Allow-Origin', label);
}
if (p.key === 'cache-control' && /html/i.test(p.source)) {
const m = /max-age\s*=\s*(\d+)/i.exec(p.value);
if (m && Number(m[1]) > 3600) {
hit(findings, text, 'html_immutable_cache', p.source, label + ' (max-age=' + m[1] + ' on ' + p.source + ')');
}
}
});
});
findings.sort(function (a, b) {
const rank = { high: 0, med: 1, low: 2 };
return (rank[a.sev] - rank[b.sev]) || (a.line - b.line);
});
return { findings: findings };
}
const OUT = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = OUT;
if (typeof window !== 'undefined') window.FDLENGINE = OUT;
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
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 FDLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = FDLREPORT;
else window.FDLREPORT = FDLREPORT;
</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.FDLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.FDLREPORT.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.firebase-deploy-leak-audit" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firebase-deploy-leak-audit',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/firebase-deploy-leak-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firebase-deploy-leak-audit',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/firebase-deploy-leak-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firebase-deploy-leak-audit',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/firebase-deploy-leak-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firebase-deploy-leak-audit',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/firebase-deploy-leak-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'firebase-deploy-leak-audit',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.firebase-deploy-leak-audit
npx @readystack/firebase-deploy-leak-audit &lt;file&gt;
npx @readystack/firebase-deploy-leak-audit --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 17 rules Firebase Deploy Leak Audit 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:"firebase-deploy-leak-audit",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">Reads firebase.json and names the files your next firebase deploy would publish to the open web</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance web-security reviewer bills around $150 an hour.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_s056vzYWEEtw0Muvdls6QOXpqf47Huzm8zNrC3JL2Ya" 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 {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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-16">Updated 2026-09-16</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-16","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/sku77708_result_card.jpg" alt="Firebase Deploy Leak Audit" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Seven high-severity findings came out of one 23-line firebase.json — the config a solo React developer gets from an AI assistant on the first ask and deploys the same afternoon.</p>
<p>The file parsed. The deploy succeeded. The site loaded. Nothing in that sequence tells you what went up with it.</p>
<p>Firebase Hosting uploads exactly what firebase.json tells it to upload, and two lines decide that: hosting.public, which names the directory, and hosting.ignore, which names what to leave behind. Firebase ships three defaults for the second one — &quot;firebase.json&quot;, &quot;**/.*&quot;, &quot;<strong>/node_modules/</strong>&quot;. Here is the part that costs people their keys: the moment you supply your own ignore array, those three are replaced, not extended.</p>
<p>So a config that reads &quot;ignore&quot;: [&quot;node_modules&quot;] looks stricter than the default and is in fact weaker than it. The dotfile pattern is gone. Every dotfile under the public root is now a file served over HTTPS at a path anyone can guess: /.env, /.env.production, /.npmrc, and the contents of /.git if the build directory is the repository root — which it is, in that sample, because public was set to &quot;.&quot;.</p>
<p>That is a generated-config failure mode specifically: an assistant optimises for a config that deploys, and the default it silently replaced is not in the text it is looking at.</p>
<p>Run the 23-line sample through the audit and 13 findings come back: 7 high, 5 medium, 1 low.</p>
<p>NaN</p>
<p>The medium ones are quieter: no Strict-Transport-Security, no X-Content-Type-Options, no framing rule — so the signed-in app can be loaded in an invisible iframe on someone else&#x27;s domain — nested node_modules still uploading file by file, and Cache-Control: max-age=31536000 on **/*.html, which is why a deploy sometimes appears not to have landed: the CDN keeps serving last week&#x27;s index.html, pointing at asset hashes that no longer exist.</p>
<p>A leaked .env is not a lint warning. It is a key rotation across every service that file named, and in the EU it starts a GDPR Article 33 clock: 72 hours from becoming aware to notify the supervisory authority. The awkward part of a Hosting leak is that &quot;becoming aware&quot; can arrive months late, because a static file served at a guessable URL leaves no signal in your own logs.</p>
<p>The audit is 17 rules over that one file, grouped three ways: where the bundle comes from, what the ignore list lets through, and what the CDN answers with. Each finding carries a severity, the line number, what an outsider gets because of it, and the exact array entry or header to add. Multi-target configs are handled — hosting can be an object or an array, and each block is reported under its own target name.</p>
<p>The same sample, corrected, returns 0 findings. That is the whole point of the pairing: the fix is eight strings in an array and one headers block, and the tool&#x27;s job is to tell you which eight.</p>
<p>Free covers one file completely — all 17 checks, every finding shown, in VS Code or on the web page, with nothing truncated, watermarked or counted down. The $29 licence changes the axis, not the depth: every firebase.json in a monorepo audited in one run, and a Markdown or JSON report you own and can commit, attach to a release, or hand to a client. A freelance web-security reviewer bills around $150 an hour.</p>
<p>Web version, no install: https://getreadystack.com/tools/firebase-deploy-leak-audit</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/sku77708_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku77708_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 Firebase Deploy Leak Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads firebase.json and applies 17 rules to it before you deploy. Seven rules cover what the ignore list lets through — .env files, source maps, service-account keys, dotfiles, node_modules. The rest cover the public root and the response headers Firebase Hosting will send. Each finding names the line, the exposure, and the entry to add.</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 it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Solo developers and two-to-five person teams shipping a React, Vue, Svelte or Next.js build to Firebase Hosting, especially when firebase.json was generated by an AI assistant or copied from a starter. It is for the person who runs firebase deploy themselves and has no reviewer between them and production.</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 a chatbot or a generic linter not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Firebase replaces its three default ignore patterns the moment you supply your own array. A generic JSON linter sees valid JSON. A chatbot reasons about the lines present in the file you paste, not about the default that silently vanished when you added one entry. This tool checks for the absence.</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 does the $29 licence add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free covers one file completely: open a firebase.json, get all 17 checks with every finding shown, nothing truncated or watermarked, in VS Code or on the web page. The licence adds a different axis — every firebase.json in a monorepo scanned in one run, and an exported Markdown or JSON report you own.</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 me otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A freelance web-security reviewer bills around $150 an hour, and a configuration review is rarely a single hour. The cheaper comparison is the incident: a leaked .env in a public bundle means rotating every key it held and, in the EU, a GDPR Article 33 notification within 72 hours of becoming aware.</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. Firebase Deploy Leak Audit checks the file you open against 17 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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 Firebase Deploy Leak Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads firebase.json and applies 17 rules to it before you deploy. Seven rules cover what the ignore list lets through — .env files, source maps, service-account keys, dotfiles, node_modules. The rest cover the public root and the response headers Firebase Hosting will send. Each finding names the line, the exposure, and the entry to add."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Solo developers and two-to-five person teams shipping a React, Vue, Svelte or Next.js build to Firebase Hosting, especially when firebase.json was generated by an AI assistant or copied from a starter. It is for the person who runs firebase deploy themselves and has no reviewer between them and production."}}, {"@type": "Question", "name": "Why is a chatbot or a generic linter not enough?", "acceptedAnswer": {"@type": "Answer", "text": "Firebase replaces its three default ignore patterns the moment you supply your own array. A generic JSON linter sees valid JSON. A chatbot reasons about the lines present in the file you paste, not about the default that silently vanished when you added one entry. This tool checks for the absence."}}, {"@type": "Question", "name": "What is free and what does the $29 licence add?", "acceptedAnswer": {"@type": "Answer", "text": "Free covers one file completely: open a firebase.json, get all 17 checks with every finding shown, nothing truncated or watermarked, in VS Code or on the web page. The licence adds a different axis — every firebase.json in a monorepo scanned in one run, and an exported Markdown or JSON report you own."}}, {"@type": "Question", "name": "What would this cost me otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "A freelance web-security reviewer bills around $150 an hour, and a configuration review is rarely a single hour. The cheaper comparison is the incident: a leaked .env in a public bundle means rotating every key it held and, in the EU, a GDPR Article 33 notification within 72 hours of becoming aware."}}, {"@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. Firebase Deploy Leak Audit checks the file you open against 17 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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:77708,slug:"firebase-deploy-leak-audit",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/view-in-browser-leak-audit"><span class="yj-rel-t">View in Browser — Publish Leak Audit (HTML)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/csaf-advisory-check-cra-2026"><span class="yj-rel-t">CSAF Advisory Check for CRA 2026</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cargo-publish-gate"><span class="yj-rel-t">Cargo.toml Publish Gate - crates.io</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firestore-rules-guard"><span class="yj-rel-t">Firestore Rules Guard</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/linker-map-auditor"><span class="yj-rel-t">Linker Map Auditor - FLASH &amp; RAM budget for GNU ld map files</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mcp-config-audit"><span class="yj-rel-t">MCP Server Config Audit</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>