Skip to content

fix(web,anchor-sdk): harden markdown rendering, add security headers, cap toml reads - #1002

Merged
determined-001 merged 2 commits into
mainfrom
fix/web-hardening
Aug 10, 2026
Merged

fix(web,anchor-sdk): harden markdown rendering, add security headers, cap toml reads#1002
determined-001 merged 2 commits into
mainfrom
fix/web-hardening

Conversation

@determined-001

Copy link
Copy Markdown
Owner

Stacked on #1001. Merge #997#998#999#1000#1001 → this.

Four smaller findings from the same audit.

1. Unsanitized docs markdown

lib/docs.ts called marked.parse() with default options — which passes raw HTML straight through — and the result went to dangerouslySetInnerHTML in app/docs/[[...slug]]/page.tsx.

lib/reference.ts had already solved this with a locked-down renderer whose comment records manual verification against <script>, onerror=, and javascript: payloads. The docs path simply never got it.

Rather than copy that renderer a second time, it moves to lib/markdownSafety.ts and both call sites use it. The two being out of step is precisely how one of them ended up unprotected; sharing the code means a future fix lands on both. resolveInternalLink stays in reference.ts and is passed in, since only the reference section rewrites .md paths onto routes.

Not remotely triggerable — the content is repo markdown. It matters because this repo merges contributor docs PRs at volume and docs diffs attract the least review of any change, so <img src=x onerror=...> in an innocuous-looking PR would become stored XSS on the docs domain.

2. No security headers

next.config.js was {}. Responses carried no CSP, HSTS, X-Frame-Options, X-Content-Type-Options, or Referrer-Policy. /demo/contracts was framable.

Adds all of them plus Permissions-Policy. 'unsafe-inline' is required for styles (the app uses inline style props throughout) and for Next's bootstrap script; 'unsafe-eval' is development-only for React Refresh. connect-src 'self' is correct — the browser only talks to this app's own SSE routes; the server is what reaches Horizon and RPC.

3. X-Powered-By

Removed. It advertised the exact framework version needed to choose from the CVE list #998 just patched.

4. Unbounded stellar.toml read

discoverAnchor called response.text() with no limit, so a hostile home domain could exhaust a consumer's memory. Now capped at 100 000 bytes, matching verifyWebhook's existing maxBodyBytes default in pulse-webhooks.

content-length is checked first, but it is a claim rather than a promise — so the cap is also enforced while streaming, and the reader is cancelled on breach instead of leaving a hostile server transmitting.

Verification

6 of 6 security headers present
x-powered-by: absent
/ /cloud /docs/... /reference /demo/contracts  ->  all 200

Docs and reference pages render unchanged — heading anchors, code blocks, and links all intact.

12 renderer tests, 3 toml-cap tests. 70 pass in anchor-sdk, 22 in apps/web.

Scope note: the renderer tests cover lib/markdownSafety.ts directly. That docs.ts uses it is verified by the build and by rendering the live page, not by a unit test.

@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbital-dev Ready Ready Preview Aug 10, 2026 12:36am

… cap toml reads

Four smaller findings from the same audit.

**Unsanitized docs markdown.** `lib/docs.ts` called `marked.parse()` with
default options, which passes raw HTML straight through, and the result went
to `dangerouslySetInnerHTML` in app/docs/[[...slug]]/page.tsx. `lib/reference.ts`
had already solved this with a locked-down renderer whose comment records
manual verification against <script>, onerror= and javascript: payloads — the
docs path simply never got it.

Rather than copy that renderer a second time, it moves to lib/markdownSafety.ts
and both call sites use it. The two being out of step is precisely how one of
them ended up unprotected; sharing the code means a future fix lands on both.
`resolveInternalLink` stays in reference.ts and is passed in, since only the
reference section rewrites .md paths onto routes.

Not remotely triggerable — the content is repo markdown. It matters because
this repo merges contributor docs PRs at volume and docs diffs attract the
least review of any change, so `<img src=x onerror=...>` in an innocuous-looking
PR would become stored XSS on the docs domain.

**No security headers.** next.config.js was `{}`, so responses carried no CSP,
HSTS, X-Frame-Options, X-Content-Type-Options or Referrer-Policy. /demo/contracts
was framable. Adds all of them plus Permissions-Policy. `'unsafe-inline'` is
required for styles (the app uses inline style props throughout) and for Next's
bootstrap script; `'unsafe-eval'` is development-only for React Refresh.
`connect-src 'self'` is correct here — the browser only talks to this app's own
SSE routes, the server is what reaches Horizon and RPC.

**X-Powered-By.** Removed. It advertised the exact framework version needed to
choose from the CVE list this branch just patched.

**Unbounded stellar.toml read.** `discoverAnchor` called `response.text()` with
no limit, so a hostile home domain could exhaust a consumer's memory. Now capped
at 100 000 bytes, matching `verifyWebhook`'s existing `maxBodyBytes` default in
pulse-webhooks. content-length is checked first, but it is a claim rather than a
promise, so the cap is also enforced while streaming and the reader is cancelled
on breach instead of leaving a hostile server transmitting.

Verified live: all six headers present, X-Powered-By absent, docs and reference
pages render unchanged (heading anchors, code blocks and links all intact).
12 renderer tests, 3 toml-cap tests; 70 pass in anchor-sdk, 22 in apps/web.

Note the renderer tests cover lib/markdownSafety.ts directly; that docs.ts uses
it is verified by the build and by rendering the live page, not by a unit test.
readCapped falls back to response.text() when a transport override returns
something Response-shaped with no readable body. That branch was untested,
which left anchor-sdk line coverage at 95.89% against a 96% floor.
@determined-001
determined-001 merged commit e48b76e into main Aug 10, 2026
27 checks passed
@determined-001
determined-001 deleted the fix/web-hardening branch August 10, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants