Skip to content

Blossom: the url a server returns is embedded into page content without a scheme check #80

Description

@usekaneo

From the CON-43 security audit (2026-09-14). Severity low — defence in depth, not an open hole.

src/nostr/blossom.ts:76-78:

const body: unknown = await response.json()
const descriptor = body as Record<string, unknown>
const url = typeof descriptor.url === 'string' ? descriptor.url : `${BLOSSOM_SERVER}/${hash}`

The only test is typeof … === 'string'. That string goes straight into attachmentMarkdown (blossom.ts:103-110) and is written into the page's Markdown as ![label](url). A Blossom server that answered {"url": "javascript:…"} would get that scheme into stored content.

Why it is low, and worth doing anyway

It does not become executable XSS: on render, the pipeline from finding A2 runs again — rehype-sanitize's protocol allowlist plus react-markdown's defaultUrlTransform — and both reject anything that is not http(s). And the Blossom server is operator-configured infrastructure (VITE_BLOSSOM_SERVER), not something an attacker supplies; anyone able to spoof it already has a bigger foothold than this app's code is defending against.

What makes it worth a small fix is the asymmetry right next to it. attachmentMarkdown is careful about the label — it strips [, ] and line breaks, with a comment explaining that a file called notes].md would otherwise end the link early and swallow the rest of the line. The url in the same template gets no equivalent care, even though a ) in it truncates the link exactly the same way. The file already knows this class of problem; it just handles one half of it.

Task

  • Accept descriptor.url only if it parses as an absolute http: or https: URL; otherwise fall back to the deterministic ${BLOSSOM_SERVER}/${hash}, which is what the code already does when the field is missing.
  • Handle a ) or whitespace in the accepted url so the generated Markdown cannot be truncated — percent-encoding it is enough, and it keeps the link working.
  • One test per case in src/nostr/blossom.test.ts: a non-http scheme falls back, a url with ) survives as a working link, a normal url is untouched.

Acceptance

  • A server response with a non-http(s) url never reaches page content.
  • A normal upload is unchanged — same url as today.
  • npm run typecheck && npm run lint && npm test && npm run build pass.

Affected: src/nostr/blossom.ts

Source: CON-43, finding A3.


Task: znjfo3t0bd7owdbrcd94oux8

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions