Skip to content

fix(ui): reject cross-site and DNS-rebinding requests to the clip ui API - #65

Merged
yultyyev merged 1 commit into
mainfrom
fix/ui-origin-guard
Nov 13, 2026
Merged

yultyyev merged 1 commit into
mainfrom
fix/ui-origin-guard

Conversation

@yultyyev

Copy link
Copy Markdown
Collaborator

What

clip ui's local API (/api/*) now turns away requests that didn't come from the UI itself:

  • Host check: a request whose Host name isn't 127.0.0.1 or localhost gets a 403. Any port is allowed, so the Vite dev proxy (which forwards the browser's localhost:5173) keeps working.
  • Origin check: Hono's built-in csrf() middleware rejects cross-site writes. It covers text/plain, form and Content-Type-less POSTs, the ones a browser sends without a CORS preflight.

Both are registered on /api/* in startUiServer (src/ui/server.ts). Static UI files are unchanged.

Why

The server binds 127.0.0.1 on a predictable port (5573) and had no request middleware. The handlers parse bodies with c.req.json(), which accepts any Content-Type. So:

  • any web page the user visits while clip ui is running could send fetch('http://127.0.0.1:5573/api/…', { method: 'POST', mode: 'no-cors', body: '{…}' }) and drive mutating endpoints (tools, timeline verbs, export, snapshot/undo, import);
  • a DNS-rebinding page (attacker domain resolving to 127.0.0.1) is same-origin from the browser's point of view, so it could also read responses.

AGENTS.md non-negotiable #3 confines clip ui callers to the workspace. That only holds if the caller is the user's own UI.

Not covered here: the snapshot-label path validation flagged on skills.sh. That's a separate change.

How to test

pnpm vitest run tests/ui-request-guard.test.ts

The new test starts the real server and sends raw HTTP with chosen Host/Origin headers. On main, the 4 attack cases all returned 200. With this change they return 403, and the 4 legitimate cases (same-origin UI write, Vite proxy write, no-Origin JSON client, localhost read) return 200.

Checks

  • pnpm test: pass, 783 passed, 24 skipped (46 files passed, 7 skipped)
  • pnpm type-check: pass
  • pnpm lint: pass, 0 errors. The 7 warnings are the same ones already on main, none in changed files.
  • pnpm build (tsup + Vite UI): pass
  • Real browser: ran the server on a test workspace and loaded the UI through vite dev (VITE_API_PORT → the test server). All UI API calls returned 200 and a same-origin snapshot POST returned 200. A no-cors cross-origin POST from http://localhost:5591 to http://127.0.0.1:5591 was refused (403) and created no snapshot.
  • Codex review (codex review --base origin/main, gpt-6.1-sol, read-only): no findings. "No actionable regressions were identified."
  • The gstack /verify wrapper needed a one-time trust grant for repo commands. Instead of granting it, I ran the same four commands above directly on this commit.

Release impact

fix(ui) → patch release on merge (angular preset).

The local API answered any request that reached 127.0.0.1:5573. A web page
the user visits could send a text/plain or Content-Type-less POST (no CORS
preflight), which the JSON-parsing handlers accepted, and a DNS-rebinding
page could read and write as if it were same-origin.

`/api/*` now rejects requests whose Host name isn't 127.0.0.1 or localhost,
and runs Hono's built-in `csrf()` so cross-site writes are refused. The Vite
dev proxy keeps working: it forwards the browser's own localhost Host and
Origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yultyyev
yultyyev merged commit 7b12e94 into main Nov 13, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.4.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant