Skip to content

fix: add siteUrl to resolve origin mismatch behind reverse proxies#382

Merged
ascorbic merged 4 commits intoemdash-cms:mainfrom
UpperM:fix/site-url-reverse-proxy-origin
Apr 11, 2026
Merged

fix: add siteUrl to resolve origin mismatch behind reverse proxies#382
ascorbic merged 4 commits intoemdash-cms:mainfrom
UpperM:fix/site-url-reverse-proxy-origin

Conversation

@UpperM
Copy link
Copy Markdown
Contributor

@UpperM UpperM commented Apr 8, 2026

What does this PR do?

Adds a siteUrl config option that fixes a class of reverse-proxy origin mismatches affecting Node.js self-hosted deployments. When EmDash runs behind a TLS-terminating proxy (nginx, Traefik, Caddy -- typical for Coolify, Dokploy, or any Docker setup), the server sees http://localhost:4321 while browsers reach it via https://mysite.example.com. PR #225 fixed this for passkeys; this PR extends the same fix to all affected call sites.

Affected subsystems (before this fix):

  • CSRF checks (form-like POSTs return 403 behind a proxy)
  • Auth and setup wizard redirects (point to the internal address)
  • OAuth redirect URIs (don't match the registered public URL)
  • MCP/CLI well-known discovery endpoints (advertise internal URLs)
  • Secure cookie flag (omitted when TLS is terminated by the proxy)
  • Snapshot export, theme preview, sitemap, robots.txt, JSON-LD

What this adds:

// astro.config.mjs
emdash({
  siteUrl: "https://mysite.example.com",
})

A single getPublicOrigin(url, config) helper wraps every affected url.origin call. When siteUrl is set, it returns that; otherwise it falls back to url.origin unchanged, so non-proxied deployments are not affected. The public origin comes from operator config or environment variables, not from request headers.

Also supports EMDASH_SITE_URL / SITE_URL env vars for container deployments where the domain is only known at runtime (using process.env rather than import.meta.env so it works after the build).

Breaking: passkeyPublicOrigin is removed. Since this is pre-release, renaming felt cleaner than maintaining both. Migration is just renaming the key in astro.config.mjs.

Decision point: checkOrigin: false

This PR disables Astro's built-in security.checkOrigin and relies on EmDash's own CSRF layer (checkPublicCsrf + X-EmDash-Request header). I want to be transparent about why and open this to discussion.

The problem: Astro's checkOrigin and allowedDomains are baked into the build manifest. Behind a reverse proxy, url.origin is the internal address and Astro's origin check blocks form-like POSTs and requests without content-type with "Cross-site POST form submissions are forbidden" -- before EmDash middleware runs. (Note: application/json requests are not affected by Astro's check, but some EmDash flows use form-like content types.) The only Astro-native solutions are:

  • allowedDomains scoped to a hostname -- works, but requires the domain at build time, which breaks Docker "build once, deploy anywhere" images where SITE_URL is set at runtime
  • allowedDomains: [{}] wildcard -- enables host header poisoning on GET responses (login redirects, OAuth, WWW-Authenticate) when SITE_URL isn't configured

The approach: Disable checkOrigin and let EmDash handle CSRF entirely. EmDash's CSRF layer handles the proxy case that Astro's check cannot: dual-origin support and runtime siteUrl resolution via env vars. When siteUrl is known at build time, allowedDomains is still set so Astro.url reflects the public origin for user template code.

The gap: User-authored form-handling routes on the same Astro site would lose Astro's CSRF protection for form submissions. (Astro's checkOrigin only covers form-like content types and requests without content-type -- it does not protect application/json API routes regardless.) In practice, EmDash sites typically don't add custom form POST endpoints (all state changes go through EmDash's API), but this is worth flagging.

Alternatives I considered but rejected:

  • Middleware interception (Astro's origin check is prepended via unshift before user middleware in the pipeline)
  • Runtime manifest patching (integration can't control the adapter)
  • Custom adapter (too invasive for this fix)

If you'd prefer a different approach -- keeping checkOrigin enabled and documenting that Docker users must set siteUrl at build time, or another path I haven't considered -- I'm happy to rework this.

Discussion #315 was opened before this implementation. I'm happy to adjust scope, naming, or approach based on maintainer feedback -- this can be split, renamed, or otherwise reshaped.

Related: #210, #225, #253, #393

Type of change

  • Bug fix
  • Feature (requires approved Discussion)
  • Refactor (no behavior change)
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

Test output
115 test files passed (2132 tests)
✓ packages/core/tests/unit/api/public-url.test.ts
✓ packages/core/tests/unit/api/csrf.test.ts
✓ packages/core/tests/unit/auth/passkey-config.test.ts
✓ packages/core/tests/unit/auth/discovery-endpoints.test.ts

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: 80dce2b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-ai-moderation Major
@emdash-cms/plugin-atproto Major
@emdash-cms/plugin-audit-log Major
@emdash-cms/plugin-color Major
@emdash-cms/plugin-embeds Major
@emdash-cms/plugin-forms Major
@emdash-cms/plugin-webhook-notifier Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Scope check

This PR changes 552 lines across 37 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@UpperM
Copy link
Copy Markdown
Contributor Author

UpperM commented Apr 8, 2026

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

UpperM added 3 commits April 11, 2026 12:48
Add `siteUrl` to EmDashConfig, replacing `passkeyPublicOrigin`. Create
`getPublicOrigin(url, config)` pure helper in api/public-url.ts that
resolves the public origin from config, EMDASH_SITE_URL / SITE_URL env
vars (at runtime via process.env), or falls back to url.origin. Both
config and env var paths validate http/https protocol only.

Extend checkPublicCsrf() with dual-origin matching so the Origin header
can match either the internal or public origin behind a reverse proxy.

Disable Astro's checkOrigin (EmDash's CSRF handles origin validation
with dual-origin and runtime siteUrl support that Astro's build-time
check cannot provide). When siteUrl is known at build time, also set
allowedDomains so Astro.url reflects the public origin in templates.

Discussion: emdash-cms#315
Replace url.origin with getPublicOrigin() across 25 files:
- Auth middleware: CSRF checks, login redirects, WWW-Authenticate
- Setup wizard + dev-bypass: store public origin as emdash:site_url
- Passkey routes (8 files): use siteUrl for rpId and origin
- OAuth: provider redirects, callback, authorize, device code
- Well-known endpoints: RFC 8414 and RFC 9728 metadata
- Snapshot export, theme preview HMAC, sitemap, robots.txt
- Page context + JSON-LD: pass siteUrl through for structured data

OAuth Secure cookie flag now checks siteUrl protocol when set,
preserving the existing fallback for non-proxy deployments.
Update public docs, skills reference, and demo config to document
siteUrl replacing passkeyPublicOrigin. Add EMDASH_SITE_URL / SITE_URL
to env vars table. Changeset: minor bump with breaking-change note.
@UpperM UpperM force-pushed the fix/site-url-reverse-proxy-origin branch from b31ab83 to e2f111f Compare April 11, 2026 10:50
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 11, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@382

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@382

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@382

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@382

emdash

npm i https://pkg.pr.new/emdash@382

create-emdash

npm i https://pkg.pr.new/create-emdash@382

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@382

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@382

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@382

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@382

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@382

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@382

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@382

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@382

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@382

commit: 80dce2b

Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

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

Great work. Thanks for this.

@ascorbic ascorbic merged commit befaeec into emdash-cms:main Apr 11, 2026
25 checks passed
@emdashbot emdashbot bot mentioned this pull request Apr 11, 2026
@UpperM UpperM deleted the fix/site-url-reverse-proxy-origin branch April 11, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants