Skip to content

feat(emdash-ui): restrict Status block fetches to an administrator-approved origin allowlist - #108

Merged
amondnet merged 2 commits into
mainfrom
amondnet/102-block-origin-allowlist
Oct 22, 2026
Merged

amondnet merged 2 commits into
mainfrom
amondnet/102-block-origin-allowlist

Conversation

@amondnet

@amondnet amondnet commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • selectFromBlock takes allowedOrigins. When the list is non-empty, a block whose base URL origin (new URL(...).origin, so case and default ports are normalised) is not on it is refused with a reason, and the renderer shows the unavailable state without fetching. The list replaces the literal-address check: a listed origin is fetched even if it is a private address, and a name that resolves to a private address is stopped unless listed. Credentials, a query and a fragment are still refused.
  • parseAllowedOrigins validates the entries. Each must be an http(s) origin with no credentials, no path beyond /, and no query or fragment. statusBeamBlock({ allowedOrigins }) throws on a bad entry, so a typo fails the site build. If a bad list reaches the renderer some other way, every block is refused and the reason names the entry.
  • With no list, or an empty one, behavior is unchanged.
  • The EmDash guide documents the option under the block registration. It notes that only the allowlist stops a hostname that resolves to a private address.

Closes #102

How the option reaches the renderer

EmDash passes a native descriptor's options to one place only: the entrypoint's createPlugin(options). It never passes them to componentsEntry or to the block components (emdash 1.3.1 source):

  • src/astro/integration/virtual-modules.ts:302-305: the generated virtual:emdash/plugins module imports createPlugin and calls it with JSON.stringify(descriptor.options ?? {}) inlined. The options are baked into the server bundle at build time, so they also reach a Workers runtime.
  • src/astro/integration/virtual-modules.ts:460-473: componentsEntry is imported bare (import { blockComponents as bcN } from "<componentsEntry>"), with no options.
  • src/components/PortableText.astro:26,49: the components are merged into <PortableText> and receive only the block data.
  • src/astro/middleware.ts:26: emdash/middleware imports virtual:emdash/plugins statically. src/astro/integration/index.ts:422-425 (buildMiddlewareEntries) always installs that middleware. createPlugin therefore runs before any page renders inside it.

So createPlugin(options) records the options with setBlockOptions (new src/block-config.ts), and StatusBlock.astro reads them at render time with renderOptions(import.meta.env.DEV). The options are stored on globalThis under Symbol.for('@statusbeam/emdash-ui/block-options'), not in a module variable. EmDash's own locals.d.ts notes that Vite SSR can load a module twice, and a module variable would split across the two copies.

Known limit: if a page renders a Status block without EmDash's middleware, createPlugin has not run. The renderer then sees no list and falls back to the literal-address guard.

Tests

  • block-node.test.ts: an allowed origin passes, including an approved private address. Another origin is refused with no fetch call. An unset or empty list keeps today's guard. Origins are compared normalised (case, :443/:80, trailing slash). Credentials, query and fragment are still refused on an allowed origin. Bad entries refuse every block, naming the entry. Also covers parseAllowedOrigins normalisation and non-array or non-string input.
  • entry-graph.test.ts: the descriptor carries normalised options, has none when the list is unset, and throws on a bad entry. createPlugin(JSON round-trip of descriptor.options), which mirrors the EmDash inlining, makes renderOptions return the list and makes selectFromBlock refuse an unlisted origin. StatusBlock.astro calls selectFromBlock(Astro.props.node ?? {}, renderOptions(import.meta.env.DEV)). That last hop is a source assertion, because bun cannot render .astro.

Gates (local)

  • bun test: 508 pass, 0 fail (36 files)
  • bun run --cwd packages/emdash-plugin test:host: 4 files, 27 tests passed
  • bun run lint: clean
  • bun run typecheck: @statusbeam/emdash-ui 0 errors. Every package passes except @statusbeam/docs and @statusbeam/web.
  • bun run build: fails only at @statusbeam/web#build.

The docs and web failures are local to this worktree and also happen with main's files: Tsconfig not found astro/tsconfigs/strict from the Vite/rolldown tsconfig resolver (Node resolves that path). main CI is green on b7f387f, so CI is the gate for those two packages.


Summary by cubic

Adds an allowedOrigins option to statusBeamBlock() so administrators can restrict which origins a Status block may fetch. A block whose base URL origin is not listed renders the unavailable state without making a request; without a configured list, production keeps the literal-address guard and dev accepts private hosts.

Passing a bad entry (non-http(s) origin, credentials, path, query, or fragment) throws at build time; a malformed list passed straight to createPlugin refuses every block. Server-side reads now run in the renderer with redirect: 'manual', so a source that redirects renders as unavailable instead of following the redirect to an unapproved origin. The options reach the renderer through createPlugin(options), which EmDash runs from its middleware before any page renders; pages rendering the block without that middleware fall back to the literal-address guard.

Written for commit dcf034e. Summary will update on new commits.

Turn on auto-fix

…proved origin allowlist

statusBeamBlock({ allowedOrigins }) validates and normalises the origins and
passes them as descriptor options. EmDash hands those only to createPlugin,
which records them on globalThis; StatusBlock.astro reads them at render
time. A block whose base URL origin is not listed is refused before any
request. With no list, the literal-address guard is unchanged.

Closes #102

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ability to restrict block sources by configuring an allowlist of trusted origins via the allowedOrigins option. This enhances security by preventing Server-Side Request Forgery (SSRF) to unapproved internal addresses. The changes include documentation, configuration management, validation logic, and corresponding tests. The review feedback suggests optimizing performance by caching the parsed origins using a WeakMap to avoid redundant URL parsing on every block render.

Comment thread packages/emdash-ui/src/block-node.ts
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@amondnet
amondnet marked this pull request as ready for review October 22, 2026 17:01
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T17:04:02.608370Z bbe30a1 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both earlier findings are fixed and no new actionable issues were found.

Summary

Adds administrator-approved origins for Status block reads, with normalized comparisons and configuration checks.

  • Configured Status blocks fetch only from approved origins.
  • Status block reads do not follow redirects to a second host.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["createPlugin(options)"] --> B["Store block options"]
  B --> C["StatusBlock reads options"]
  C --> D{"selectFromBlock accepts source?"}
  D -->|No| E["Show unavailable"]
  D -->|Yes| F["Fetch without following redirects"]
  F --> G["Pass result to component"]
  G --> H["Show status or unavailable"]
Loading

Reviews (2) · Last reviewed commit: "fix(emdash-ui): keep Status block reads ..." · Reviewed by Greptile

Comment thread packages/emdash-ui/src/block-node.ts Outdated
Comment thread packages/emdash-ui/src/block-node.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/emdash-ui/src/block-node.ts">

<violation number="1" location="packages/emdash-ui/src/block-node.ts:184">
P2: `selectFromBlock` does not fail closed for malformed renderer options: values such as `allowedOrigins: ''` or `0` bypass parsing and restore the literal-host guard, while `null` throws during rendering. Validate every defined value before checking its length, then use the parsed list length so only a valid empty list keeps the fallback behavior.</violation>
</file>

Linked issue analysis

Linked issue: pleaseai/statusbeam#102: feat(emdash-ui): restrict Status block fetches to an administrator-approved origin allowlist

Status Acceptance criteria Notes
✅ Refuse unlisted origins without fetching selectFromBlock rejects origins outside the configured list, and the tests verify no fetch occurs.
✅ Keep the literal-address guard without an allowlist With no list or an empty list, selection uses the existing private-host check; tests cover both cases.
✅ Document the allowlist setting in the EmDash guide The guide shows the allowedOrigins configuration and explains validation, matching, and fallback behavior.
Architecture diagram
sequenceDiagram
    participant Dev as Site Developer
    participant Config as astro.config.mjs
    participant Desc as statusBeamBlock()
    participant Build as EmDash Build (Vite)
    participant Plugin as createPlugin()
    participant Store as globalThis Symbol Registry
    participant MW as EmDash Middleware
    participant Block as StatusBlock.astro
    participant Select as selectFromBlock()
    participant Fetch as fetch() (Server)
    participant UI as Browser

    Note over Dev,Config: Site Configuration
    Dev->>Config: allowedOrigins: ['https://status.example.com']
    Config->>Desc: statusBeamBlock({ allowedOrigins })
    Desc->>Desc: parseAllowedOrigins() normalizes entries
    alt Valid entries
        Desc-->>Desc: Normalized origin list
        Desc-->>Build: Descriptor with options
    else Invalid entry
        Desc-->>Desc: Throws Error (build fails)
    end

    Note over Build,MW: Build-time Plugin Wiring
    Build->>Plugin: Calls createPlugin(options) with inlined JSON
    Plugin->>Store: setBlockOptions(options) on Symbol.for('@statusbeam/emdash-ui/block-options')
    Build->>MW: Static import of virtual:emdash/plugins

    Note over MW,Block: Runtime Request Flow
    UI->>MW: Page request
    MW->>Store: Plugin already initialized (options available)
    MW-->>Block: Render PortableText with block data

    Block->>Select: selectFromBlock(node, renderOptions(dev))
    Select->>Store: Reads allowedOrigins from globalThis registry
    alt allowedOrigins configured and non-empty
        Select->>Select: Check baseUrl origin against allowlist
        alt Origin is allowed
            Select->>Fetch: Fetch status data (credentials/query/fragment still refused)
            Fetch-->>Select: Status data
        else Origin not allowed
            Select-->>Block: Unavailable state (no fetch)
        end
    else No list or empty list
        Select->>Select: Fallback: literal-address check
        alt Private/literal address
            Select-->>Block: Unavailable state
        else Public address
            Select->>Fetch: Fetch status data
            Fetch-->>Select: Status data
        end
    end
    Select-->>Block: Selection result
    Block-->>UI: Rendered status block (banner/site/detail)
Loading

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread packages/emdash-ui/src/block-config.ts
Comment thread packages/emdash-ui/src/block-node.ts Outdated
Comment thread apps/docs/src/content/docs/guides/emdash.mdx Outdated
The origin checks in selectFromBlock saw only the configured URL, while
the read followed redirects, so an accepted source could send the server
to an origin nobody approved. The block renderer now does the read
itself with redirect: 'manual'; a source that redirects renders as
unavailable (HTTP 3xx) and editors must enter its final URL.

Any defined allowedOrigins value is now parsed, so a malformed one
passed straight to createPlugin refuses every block instead of falling
back to the literal-address check. An unread non-2xx body is cancelled.
The guide now scopes the literal-address check to production builds.
@amondnet

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@sonarqubecloud

Copy link
Copy Markdown

@amondnet
amondnet merged commit 6c086f2 into main Oct 22, 2026
13 checks passed
@amondnet
amondnet deleted the amondnet/102-block-origin-allowlist branch October 22, 2026 18:17
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.

feat(emdash-ui): restrict Status block fetches to an administrator-approved origin allowlist

1 participant