Skip to content

Repo: /home/ta/projects/monorepo/ — Astro multi-site + SvelteKit PWA + FastAPI Date: 2026-05-14 Scope: Fast pre-production triage


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: CORSMiddleware is configured with allow_origins=['*'], allow_methods=['*'], allow_headers=['*']. Although allow_credentials is 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:
    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"],
    Drive the list from an env var so prod/staging differ.

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: only X-Content-Type-Options, Referrer-Policy, and a frame-ancestors-only CSP).
  • What: The Content-Security-Policy directive present only sets frame-ancestors. There is no default-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', and form-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, restricting connect-src to the actual sd-api origin (not 'self' alone — paraglide + Fathom etc. may need to be allow-listed):
    Content-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'
    Then drop 'unsafe-inline'/'unsafe-eval' from script-src if 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 under git ls-files. The .gitignore rule .wrangler/ exists at sites/template/.gitignore:6 but 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:
    Terminal window
    git rm -r --cached sites/template/.wrangler
    git commit -m "chore: untrack .wrangler dev artifacts"
    Apply same audit to apps/sd-app/.wrangler/ and sites/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 with apps/sd-app/.gitignore which has .env / .env.* / !.env.example).
  • What: Inspection of sites/mbr/.gitignore (line 6 has .wrangler per grep output, but no .env rule shown). The root .gitignore does cover .env / .env.*, which inherits — but a per-site rule was added for sd-app and template, and the mbr site is new (recent commit 90ffe06 “add mbr site”). Defense-in-depth: every new site should re-declare .env ignores in its own .gitignore to survive future repo restructuring.
  • Why: Root .gitignore rules 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" and id = "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 in wrangler.toml, secrets go to wrangler 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 here to 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 or nonce-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: svgInner is extracted via regex from svgContent and rendered with set:html. Currently safe because svgContent is 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 the string branch returns the raw value and feeds it to set:html. If a future page passes user-controlled or CMS-controlled strings as feature.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. Keep set:html only 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 textContent for 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.

  • Root .gitignore correctly excludes .env / .env.* with !.env.example exception.
  • 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.example files (sites/template/.env.example, sites/mbr/.env.example) contain only placeholder values.
  • Python packages (sd-api, sd-math) show no pickle.loads, eval, exec, shell=True, or os.system usage.
  • Template site CSP is comprehensive (only 'unsafe-eval' is excessive — see M1).
  • _worker.js redirects use URL constructor (no open-redirect via untrusted input).
  • Husky pre-commit runs lint-staged; pre-push runs unit + TS + E2E (chromium).

  1. Before next deploy: Fix C1 (CORS lockdown) — single-file change in packages/sd-api/main.py.
  2. Before next deploy: Fix I1 (sd-app CSP) — add full CSP to apps/sd-app/static/_headers.
  3. This week: I2 (untrack .wrangler/), I3 (mbr .gitignore), I4 (wrangler.toml comment).
  4. Backlog: All Minor items together as a CSP-hardening + XSS-defense-in-depth PR.