Repository navigation
feat(emdash-ui): restrict Status block fetches to an administrator-approved origin allowlist - #108
Conversation
…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
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
[Medium impact] The PR appears safe to merge; both earlier findings are fixed and no new actionable issues were found. SummaryAdds administrator-approved origins for Status block reads, with normalized comparisons and configuration checks.
|
There was a problem hiding this comment.
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)
Reply with feedback, questions, or to request a fix.
Turn on auto-fix | Re-trigger cubic
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.
|
/gemini review |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|



Summary
selectFromBlocktakesallowedOrigins. 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.parseAllowedOriginsvalidates 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.Closes #102
How the option reaches the renderer
EmDash passes a native descriptor's
optionsto one place only: the entrypoint'screatePlugin(options). It never passes them tocomponentsEntryor to the block components (emdash 1.3.1 source):src/astro/integration/virtual-modules.ts:302-305: the generatedvirtual:emdash/pluginsmodule importscreatePluginand calls it withJSON.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:componentsEntryis 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/middlewareimportsvirtual:emdash/pluginsstatically.src/astro/integration/index.ts:422-425(buildMiddlewareEntries) always installs that middleware.createPlugintherefore runs before any page renders inside it.So
createPlugin(options)records the options withsetBlockOptions(newsrc/block-config.ts), andStatusBlock.astroreads them at render time withrenderOptions(import.meta.env.DEV). The options are stored onglobalThisunderSymbol.for('@statusbeam/emdash-ui/block-options'), not in a module variable. EmDash's ownlocals.d.tsnotes 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,
createPluginhas 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 nofetchcall. 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 coversparseAllowedOriginsnormalisation 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, makesrenderOptionsreturn the list and makesselectFromBlockrefuse an unlisted origin.StatusBlock.astrocallsselectFromBlock(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 passedbun run lint: cleanbun run typecheck:@statusbeam/emdash-ui0 errors. Every package passes except@statusbeam/docsand@statusbeam/web.bun run build: fails only at@statusbeam/web#build.The
docsandwebfailures are local to this worktree and also happen with main's files:Tsconfig not found astro/tsconfigs/strictfrom the Vite/rolldown tsconfig resolver (Node resolves that path). main CI is green onb7f387f, so CI is the gate for those two packages.Summary by cubic
Adds an
allowedOriginsoption tostatusBeamBlock()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
createPluginrefuses every block. Server-side reads now run in the renderer withredirect: 'manual', so a source that redirects renders as unavailable instead of following the redirect to an unapproved origin. The options reach the renderer throughcreatePlugin(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.