Monorepo Security Review
Section titled “Monorepo Security Review”Repo: /home/ta/projects/monorepo/ — Astro multi-site + SvelteKit PWA + FastAPI
Date: 2026-05-14
Scope: Fast pre-production triage
Critical
Section titled “Critical”C1. FastAPI service has wildcard CORS with permissive methods/headers
Section titled “C1. FastAPI service has wildcard CORS with permissive methods/headers”- Where:
packages/sd-api/main.py:11-16 - What:
CORSMiddlewareis configured withallow_origins=['*'],allow_methods=['*'],allow_headers=['*']. Althoughallow_credentialsis not set (defaults to False, which prevents the worst cookie-theft scenarios), any web origin can call the API in any browser. If the API ever gains authenticated endpoints, billing, or rate-limited operations, this is open abuse. - Why: Wildcard CORS prevents the browser from being a useful first line of defense and undermines any future auth model. It also makes embedding/scraping by third-party UIs trivial, which is often unintended.
- Fix: Restrict to known origins before production:
Drive the list from an env var so prod/staging differ.allow_origins=["https://sd-app-eu1.pages.dev","https://smartdebtcoach.com","https://site-template-183.pages.dev",],allow_methods=["GET", "POST"],allow_headers=["Content-Type"],
Important
Section titled “Important”I1. SvelteKit sd-app has NO Content-Security-Policy header
Section titled “I1. SvelteKit sd-app has NO Content-Security-Policy header”- Where:
apps/sd-app/static/_headers(entire file is 3 lines: onlyX-Content-Type-Options,Referrer-Policy, and aframe-ancestors-only CSP). - What: The
Content-Security-Policydirective present only setsframe-ancestors. There is nodefault-src,script-src,connect-src,object-src, etc. Template site has a full CSP (sites/template/public/_headers) — sd-app is missing it. - Why: sd-app is a calculator handling user-input investment data; lack of
default-src 'self',object-src 'none',base-uri 'self', andform-action 'self'leaves it exposed to script injection escalation if any XSS sink is introduced. - Fix: Mirror the template’s CSP in
apps/sd-app/static/_headers, restrictingconnect-srcto the actual sd-api origin (not'self'alone — paraglide + Fathom etc. may need to be allow-listed):Then dropContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://<sd-api-host>; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self''unsafe-inline'/'unsafe-eval'fromscript-srcif Astro/SvelteKit hashes permit. Note: template’s CSP also includes'unsafe-eval'which is broader than needed (see Minor M1).
I2. .wrangler/ build artifacts are committed in sites/template/
Section titled “I2. .wrangler/ build artifacts are committed in sites/template/”- Where:
sites/template/.wrangler/tmp/...— 10 files tracked undergit ls-files. The.gitignorerule.wrangler/exists atsites/template/.gitignore:6but these files were committed before the rule was added and have not been purged. - What: These are wrangler-generated dev bundles. Currently they don’t appear to contain secrets (grep found none), but
.wrangler/directories can include cached account state, KV/D1 snapshots, and local secrets in some configurations. Continued accumulation risks future leakage. - Why: Pattern of committing dev-server artifacts; one bad cache state away from leaking a token or local DB row.
- Fix:
Apply same audit to
Terminal window git rm -r --cached sites/template/.wranglergit commit -m "chore: untrack .wrangler dev artifacts"apps/sd-app/.wrangler/andsites/mbr/.wrangler/(currently untracked — good, keep that way).
I3. sites/mbr/.gitignore does not ignore .env files
Section titled “I3. sites/mbr/.gitignore does not ignore .env files”- Where:
sites/mbr/.gitignore(compared withapps/sd-app/.gitignorewhich has.env/.env.*/!.env.example). - What: Inspection of
sites/mbr/.gitignore(line 6 has.wranglerper grep output, but no.envrule shown). The root.gitignoredoes cover.env/.env.*, which inherits — but a per-site rule was added for sd-app and template, and the mbr site is new (recent commit90ffe06“add mbr site”). Defense-in-depth: every new site should re-declare.envignores in its own.gitignoreto survive future repo restructuring. - Why: Root
.gitignorerules don’t follow if the directory is moved or extracted to its own repo (highly relevant given site-per-repo extraction patterns). - Fix: Append to
sites/mbr/.gitignore:.env.env.*!.env.example
I4. wrangler.toml contains placeholder IDs that look like they could become real
Section titled “I4. wrangler.toml contains placeholder IDs that look like they could become real”- Where:
sites/mbr/wrangler.toml:7,11 - What:
database_id = "REPLACE_WITH_D1_DATABASE_ID"andid = "REPLACE_WITH_KV_NAMESPACE_ID". While these are explicit placeholders (good), Cloudflare D1 binding IDs/KV namespace IDs are not secret per se, but they have historically been used to enumerate accounts and target reconnaissance. Documented convention should be: real IDs go inwrangler.toml, secrets go towrangler secret put, and never inline tokens. - Why: Risk is replacement with real IDs followed by accidental further commits of
[env.production]blocks with API tokens inline (a common mistake). - Fix: Add a comment
# Set via: wrangler secret put — never inline tokens hereto the file, and consider moving binding IDs to a.dev.vars-style approach (not strictly required, but reduces surface).
M1. Template CSP allows 'unsafe-eval' in script-src
Section titled “M1. Template CSP allows 'unsafe-eval' in script-src”- Where:
sites/template/public/_headers:6 - What:
script-src 'self' 'unsafe-inline' 'unsafe-eval'—'unsafe-eval'is rarely needed in an Astro static site.'unsafe-inline'is already weakening the policy. - Fix: Drop
'unsafe-eval'first (low risk). Then attempt to remove'unsafe-inline'by switching inline scripts to hashed ornonce-based (Cloudflare Pages supports this via Functions middleware).
M2. BrandLogo.astro uses set:html on regex-extracted SVG content
Section titled “M2. BrandLogo.astro uses set:html on regex-extracted SVG content”- Where:
sites/template/src/components/brand/BrandLogo.astro:119,137 - What:
svgInneris extracted via regex fromsvgContentand rendered withset:html. Currently safe becausesvgContentis imported from local trusted brand assets, not user input. Flagging only because the pattern is fragile — if a future contributor wires user/CMS-provided SVG through this component, it’s an XSS sink. - Fix: Add a comment at line 119 noting the trust assumption (
// SVG source must be a trusted build-time import — never user input). No code change needed today.
M3. ComparisonTable.astro set:html={renderValue(...)} renders trusted-but-fragile HTML
Section titled “M3. ComparisonTable.astro set:html={renderValue(...)} renders trusted-but-fragile HTML”- Where:
sites/template/src/components/blocks/ComparisonTable.astro:31-38, 70-76 - What:
renderValue()returns hardcoded HTML strings for booleans, but for thestringbranch returns the rawvalueand feeds it toset:html. If a future page passes user-controlled or CMS-controlled strings asfeature.basic/pro/enterprise, it’s an XSS sink. - Fix: Change the string branch to plain text rendering (return the string and use
{value}in template) OR explicitly escape. Keepset:htmlonly for the boolean check/x marks.
M4. Search.astro uses innerHTML for SVG insertion
Section titled “M4. Search.astro uses innerHTML for SVG insertion”- Where:
packages/components/src/ui/Search.astro:232,252 - What: Hardcoded literal HTML, no user input — safe today. Same fragility concern as M2/M3.
- Fix: Use
textContentfor the ESC hint (line 252 — it’s literally a text string with a<kbd>tag; if you don’t need the tag, prefer textContent).
M5. Pre-push hook only runs chromium E2E, skipping mobile/webkit
Section titled “M5. Pre-push hook only runs chromium E2E, skipping mobile/webkit”- Where:
.husky/pre-push:13 - What: Not a security flaw per se, but mobile-specific accessibility/contrast regressions ship without local gate. (Documented elsewhere as known tradeoff.)
- Fix: N/A — accepted tradeoff per CLAUDE.md.
Positives Observed
Section titled “Positives Observed”- Root
.gitignorecorrectly excludes.env/.env.*with!.env.exampleexception. - No secrets matched standard patterns (
AKIA…,ghp_…,sk-…,xox[…]) in tracked files. git log -S 'API_KEY' / 'SECRET'hits are only documentation files (docs/SECRETS.md), not real credentials..env.examplefiles (sites/template/.env.example,sites/mbr/.env.example) contain only placeholder values.- Python packages (
sd-api,sd-math) show nopickle.loads,eval,exec,shell=True, oros.systemusage. - Template site CSP is comprehensive (only
'unsafe-eval'is excessive — see M1). _worker.jsredirects useURLconstructor (no open-redirect via untrusted input).- Husky
pre-commitruns lint-staged;pre-pushruns unit + TS + E2E (chromium).
Recommended Order
Section titled “Recommended Order”- Before next deploy: Fix C1 (CORS lockdown) — single-file change in
packages/sd-api/main.py. - Before next deploy: Fix I1 (sd-app CSP) — add full CSP to
apps/sd-app/static/_headers. - This week: I2 (untrack
.wrangler/), I3 (mbr.gitignore), I4 (wrangler.toml comment). - Backlog: All Minor items together as a CSP-hardening + XSS-defense-in-depth PR.