Repository navigation
SEC-002: one SSRF address policy, an unset inbound secret refuses, stored feed links open only as web links - #983
Conversation
The app had two SSRF address policies. The calendar/feed fetcher (calendar sync, feed sync, weekend discovery, social link previews) kept its own range list, which had drifted from the document fetcher's: a hostname resolving to 6to4 wrapping 169.254.169.254, Teredo, SIIT, site-local, the discard prefix, 3fff::/20, the 6to4 relay anycast or the Azure platform endpoint passed it. Both now use one policy, lib/server/public-address.ts. The calendar fetcher still unwraps an IPv4 address carried in IPv6 (mapped, compatible, NAT64) and judges it as that IPv4 address, so NAT64 to a public host keeps working. New test: 16 failures on the old source, 20/20 after. The source-shape guard now asserts the shared import instead of a private range literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
/api/contact-center/email is a public callback in middleware.ts, so the route's own check is all that stands in front of it. With CONTACT_CENTER_INBOUND_SECRET unset it let every request through unless NODE_ENV was 'production': a dev or self-hosted server run without that filed anyone's mail into a family's inbox, ran the concierge on it and sent an auto-reply to an address the sender chose. Deployed `next build` output inlines NODE_ENV=production, so hosted builds already refused. Now an unset secret refuses whatever NODE_ENV says, as the Twilio ingress already does. New test: 6 failures on the old source (development, test, empty NODE_ENV), 9/9 after; all 68 contact-center suites pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
The Social Feed's "Open post" passed social_reader_items.permalink straight to window.open. Any active family member (child and guest included) can write that column under the 0101 policy, and addFeedItemAction stored permalink, thumbnail and media URLs unchecked. React's javascript: filtering covers href, not window.open; whether a javascript: URL then ran depended on the browser's noopener handling. The module opens the permalink through safeSocialLink (the SEC-004 rule every stored href already uses) and shows "Open post" only for a web link; the action refuses a non-web permalink, thumbnail or media URL before writing. A new ratchet holds every window.open call to a literal, a fixed-host template or a safe*Link value. New test: 6 failures on the old source, 8/8 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
Redirects, validation, secrets, cross-site and uploads traced end to end on main 36a516d. Three Low findings fixed in this branch with before/after tests; five recorded with a proposed owner and scope (two need a storage migration, one an operator decision on /api/health). The register row stays IN PROGRESS: browser workflow cases were not run and R1-R3 remain open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
SEC-002 scoped review at5e70bb2288c1a15c4e72807353e2e48795e94ad6 found the changed runtime boundaries correctly wired; exact-head CI remains separate. Bounded follow-on for this lane: tighten tests/a-stored-link-is-opened-only-as-a-web-link.test.ts so the global ratchet does not trust an arbitrary variable named href or accept a string-literal prefix followed by unsafe concatenation. Inspect the full argument/binding or narrow the stated guarantee; add mutation controls for unvalidated href, concatenation and an additional unsafe window.open call. The actual Social Feed sink is correctly guarded; this is a future-regression detection gap. Also update the inbound-email route header that still claims an unset secret is allowed in development. Keep SEC-002, release/storage/operator holds and browser evidence gaps open. Public address-policy sharing does not make calendar/document transports identical. No production configuration changes or live external probes are requested. |
…pelling Owner review on #983: the ratchet's regex trusted any variable named href and any string literal followed by concatenation. It now parses each file and accepts only a literal, a fixed-scheme or fixed-host template, a safeWebLink/safeSocialLink call, or a const bound to one of those in an enclosing block. Synthetic controls refuse a stored field bound to href, a parameter, a let, a concatenation after a literal prefix, a stored-field template, a conditional and an unknown call, and report an unsafe call added to the real file set. Against the original Social Feed module it reports social-feed-module.tsx:220. Also: the inbound-email route header no longer says an unset secret is allowed in development, and the evidence doc says only the address ranges are shared, not the transports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
|
Re 6092751883: both items are in Ratchet (
Everything else fails, including any concatenation. Controls:
26/26 pass on the branch. Route header. Transports. The evidence doc now says only the address ranges are shared. The calendar fetcher still allows plain http, unwraps mapped/NAT64 IPv4 before applying the policy, and keeps its own redirect and size handling. Checks: eslint, SEC-002 stays IN PROGRESS. The release, storage and operator holds (R1–R3) and the browser evidence gaps stay open. No production configuration or live external probes. Generated by Claude Code |
|
Follow-on to6092751883 at4fe0c9b: the original unsafe href/concatenation cases and documentation are source-corrected. One bounded Low test-assurance residual remains in constInitializer: a nearer parameter, let/var or destructured binding can be skipped in favor of an outer safe const with the same name. Add controls combining that outer safe const with each unsafe inner shadow, and resolve the actual lexical binding or stop conservatively at any nearer binding. This concerns the ratchet's detection guarantee; no unsafe changed Social Feed runtime path was found. Keep guard-import identity outside any claim of complete semantic provenance unless verified. No additional source/security scope or live probes requested. |
…s to Owner follow-on on #983: constInitializer looked only for const declarations, so a nearer parameter, let, var or destructured binding was skipped in favour of an outer safe const of the same name. The ratchet now resolves the nearest scope that declares the name (parameters, let/const, hoisted var, destructuring, catch and loop bindings, imports) and accepts an identifier only when that binding is a plain const declared before the use with a safe initializer. A guard call counts only when its name is bound by an import of that name from @/lib/utils/safe-link or @/lib/social/links. New controls: an outer safe const shadowed by a parameter, arrow parameter, destructured parameter, inner let, hoisted var, destructured const, catch or for-of binding is refused; a const used before declaration is refused; a local look-alike, unimported, other-module, renamed or parameter-shadowed guard is refused. 8 of the shadowing controls and the guard control fail on the previous resolver; 38/38 pass now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
…t rule @next/next/no-assign-module-variable refuses a local named `module`; the guard's source module is now `source`. No behaviour change: 38/38 still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
|
Re 6092846875: the binding fix is in Resolution. An identifier now resolves to the nearest enclosing scope that declares its name. Those scopes are parameters, Guard identity. A New controls, outer safe
These are refused too:
Accepted: the unshadowed outer Against the previous resolver. I ran the same controls against the resolver from Checks:
The Social Feed runtime path is unchanged. SEC-002 stays IN PROGRESS with R1–R3 and the browser gaps open. Generated by Claude Code |
SEC-002 (High, not started) covers redirects, validation, secrets, cross-site and uploads. Three parallel read-only sweeps traced every sink on
main36a516d. None found a High or Medium issue. This PR fixes the three Low findings that need only code, each with a test that fails on the old source. The full write-up is indocs/audit/sec-002-sweep-20261009.md.Fixes
2002:a9fe:a9fe::1, i.e. 169.254.169.254), Teredo, SIIT,fec0::/10,100::/64,3fff::/20,192.88.99.0/24and168.63.129.16. The guard covers calendar sync, feed sync, weekend discovery and link previews.lib/server/public-address.ts. The document fetcher re-exports it. The calendar fetcher still unwraps mapped/NAT64 IPv4, so NAT64 to a public host still works./api/contact-center/email, a public callback, accepted every request whenCONTACT_CENTER_INBOUND_SECRETwas unset andNODE_ENVwas notproduction.permalinktowindow.openunchecked, andaddFeedItemActionstored links unchecked.safeSocialLink, the action refuses non-web links, and a new ratchet holds everywindow.opento a literal or a safe link.Recorded, not fixed here
documentsandchore-proofbuckets have noallowed_mime_types. An SVG with script would run on the Supabase origin when a parent opens its signed URL. Fixing it needs a bucket migration./api/healthnames unset secrets. Changing that is an operator decision, since Health reports RESEND_WEBHOOK_SECRET, whose absence refuses every bounce and complaint (ENV-C472B8258F2A) #877 added the list on purpose.X-Forwarded-Host, and a message cursor is not UUID-checked.Out of scope, and the next task: kids' proof photos and videos go through a server action whose body is capped at 1 MB by default. That refuses most phone photos and every video with a 413.
The register row stays IN PROGRESS. The browser workflow cases were not run, and R1–R3 are still open. Nothing here touches the auth/session-cache files, migrations, configuration or production.
Validation
whole-table-reads-are-not-cappedhitting its 20 s timeout under full parallel load. It passes 40/40 when run alone, and this branch does not touch it.tsc --noEmitexit 0, eslint clean on changed files,git diff --checkclean. The docs-migration-name guard passes 616/616.🤖 Generated with Claude Code
https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
Generated by Claude Code