Skip to content

fix(session): harden browser request checks#335

Closed
benvinegar wants to merge 1 commit into
mainfrom
fix/session-browser-csrf-hardening
Closed

fix(session): harden browser request checks#335
benvinegar wants to merge 1 commit into
mainfrom
fix/session-browser-csrf-hardening

Conversation

@benvinegar
Copy link
Copy Markdown
Member

@benvinegar benvinegar commented May 19, 2026

Summary

  • Reject browser-originated daemon requests with disallowed Fetch Metadata (cross-site / same-site) before they reach HTTP or websocket handlers.
  • Reject browser-looking requests that carry Sec-* browser headers but omit Origin.
  • Require x-hunk-session-client: hunk-cli on /session-api posts, and send it from the Hunk session CLI.
  • Bump the Hunk session daemon compatibility version and update the manual curl probe docs.

Validation

  • bun run typecheck
  • bun run format:check
  • bun run lint
  • bun test
  • Real detached tmux smoke test for hunk session list/context/review/navigate/comment.
  • Raw /session-api POST without marker header returns 403; same request with x-hunk-session-client: hunk-cli returns 200.

This PR description was generated by Pi using GPT-5

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR adds layered browser-request hardening to the local session daemon: Fetch Metadata checks block cross-site/same-site browser traffic, a secondary check catches browser-looking requests (Sec-* headers) that omit an Origin, and a new x-hunk-session-client marker header gates the /session-api command endpoint. The HUNK_SESSION_DAEMON_VERSION is bumped to 4 to signal the new requirements to older CLI clients.

  • brokerServer.ts: Three new request-validation functions (validateFetchMetadata, validateBrowserRequestOrigin, validateSessionClientHeader) are inserted into the global handler (before routing) and the session-API handler respectively; the layered logic is correct — none and same-origin pass the Fetch Metadata check but are still caught by the browser-origin check if no Origin header is present.
  • cli.ts / protocol.ts: The CLI now sends x-hunk-session-client: hunk-cli on every POST, and the constants are exported from the protocol module so both sides stay in sync.
  • Tests: Three new integration tests cover the rejection paths, and all existing session-API tests are updated to include the new required header.

Confidence Score: 4/5

Safe to merge; the request-validation logic is correct and the tests cover the new rejection paths end-to-end.

The three new validator functions work together correctly — the interaction where sec-fetch-site: none passes validateFetchMetadata but is then caught by validateBrowserRequestOrigin is intentional and sound. The only gaps are two misleading JSDoc comment phrasings and a missing positive-case assertion for allowed Fetch-Metadata values, which could mask a future regression in the allow-list logic.

brokerServer.test.ts would benefit from positive-case tests confirming that sec-fetch-site: none and sec-fetch-site: same-origin are permitted through validateFetchMetadata.

Important Files Changed

Filename Overview
src/session-broker/brokerServer.ts Adds three new request-validation functions (validateFetchMetadata, validateBrowserRequestOrigin, validateSessionClientHeader) and wires them into the global handler and the session-API handler; the layered logic is correct but the JSDoc wording on the two new global validators is misleading.
src/session-broker/brokerServer.test.ts Adds three new integration tests for Fetch-Metadata, browser-origin, and client-header checks, and updates existing tests with SESSION_API_HEADERS; positive-case coverage (sec-fetch-site: none / same-origin allowed) is missing.
src/session/protocol.ts Adds HUNK_SESSION_CLIENT_HEADER / HUNK_SESSION_CLIENT_HEADER_VALUE constants and bumps HUNK_SESSION_DAEMON_VERSION to 4; straightforward protocol additions.
src/hunk-session/cli.ts Adds the x-hunk-session-client header to every outbound POST; clean, minimal change.
src/hunk-session/cli.test.ts Extends existing test to assert the new client header is sent on POST requests; correct and complete for the CLI path.
docs/agent-workflows.md Adds the required x-hunk-session-client header to the manual curl probe example; matches the new server requirement.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    REQ([Incoming Request]) --> HOST[validateHostHeader]
    HOST -->|error| R400[400/403]
    HOST -->|pass| ORIGIN[validateOriginHeader]
    ORIGIN -->|error| R403A[403]
    ORIGIN -->|pass| FM[validateFetchMetadata\nsec-fetch-site present?]
    FM -->|none or same-origin| BRO[validateBrowserRequestOrigin\nSec-* headers w/o Origin?]
    FM -->|cross-site / same-site / unknown| R403B[403]
    FM -->|absent| BRO
    BRO -->|Sec-* headers, no Origin| R403C[403]
    BRO -->|no Sec-* headers, or Origin present| ROUTE{Route}
    ROUTE -->|/health| HEALTH[200 Health JSON]
    ROUTE -->|/session-api/capabilities| CAPS[200 Capabilities JSON]
    ROUTE -->|/session-api POST| CLIHDR[validateSessionClientHeader\nx-hunk-session-client?]
    ROUTE -->|WebSocket| WS[WebSocket Handler]
    CLIHDR -->|missing/wrong| R403D[403]
    CLIHDR -->|hunk-cli| CT[hasJsonContentType?]
    CT -->|no| R415[415]
    CT -->|yes| ACTION[Process Action]
    ACTION --> RESP([Response])
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/session-broker/brokerServer.ts:101-102
The JSDoc comments on the two new validators use "Return whether..." phrasing, which implies a boolean return value. Both functions actually return `Response | null` (a pattern consistent with every other validator in this file). The existing validators (`validateHostHeader`, `validateOriginHeader`) use action-oriented "Block..." phrasing that makes the semantics clear.

```suggestion
/** Block requests whose Sec-Fetch-Site indicates cross-site or same-site browser traffic. */
function validateFetchMetadata(request: Request) {
```

### Issue 2 of 3
src/session-broker/brokerServer.ts:115-116
Same "Return whether..." phrasing issue as `validateFetchMetadata`. The function rejects the request (returns a 403 Response) rather than returning a boolean predicate.

```suggestion
/** Block browser-looking requests that carry Sec-* headers but omit an Origin. */
function validateBrowserRequestOrigin(request: Request) {
```

### Issue 3 of 3
src/session-broker/brokerServer.test.ts:382-420
**Missing positive-case coverage for allowed Fetch-Metadata values**

The test verifies that `cross-site` (HTTP) and `same-site` (WebSocket) are rejected, but there are no assertions that `sec-fetch-site: none` or `sec-fetch-site: same-origin` are explicitly allowed through `validateFetchMetadata`. Because `sec-fetch-site: none` is allowed by `validateFetchMetadata` but then caught by `validateBrowserRequestOrigin` (no Origin + Sec-* header present), the two functions interact subtly. A regression that accidentally tightens `validateFetchMetadata` to also block `none` would be invisible without a positive test exercising that path.

Reviews (1): Last reviewed commit: "fix(session): harden browser request che..." | Re-trigger Greptile

Comment thread src/session-broker/brokerServer.ts Outdated
Comment on lines +101 to +102
/** Return whether Fetch Metadata identifies this request as browser cross-site traffic. */
function validateFetchMetadata(request: Request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The JSDoc comments on the two new validators use "Return whether..." phrasing, which implies a boolean return value. Both functions actually return Response | null (a pattern consistent with every other validator in this file). The existing validators (validateHostHeader, validateOriginHeader) use action-oriented "Block..." phrasing that makes the semantics clear.

Suggested change
/** Return whether Fetch Metadata identifies this request as browser cross-site traffic. */
function validateFetchMetadata(request: Request) {
/** Block requests whose Sec-Fetch-Site indicates cross-site or same-site browser traffic. */
function validateFetchMetadata(request: Request) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/session-broker/brokerServer.ts
Line: 101-102

Comment:
The JSDoc comments on the two new validators use "Return whether..." phrasing, which implies a boolean return value. Both functions actually return `Response | null` (a pattern consistent with every other validator in this file). The existing validators (`validateHostHeader`, `validateOriginHeader`) use action-oriented "Block..." phrasing that makes the semantics clear.

```suggestion
/** Block requests whose Sec-Fetch-Site indicates cross-site or same-site browser traffic. */
function validateFetchMetadata(request: Request) {
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

@benvinegar benvinegar May 19, 2026

Choose a reason for hiding this comment

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

Updated the comment to use action-oriented wording that matches the validator's Response/null behavior.

This comment was generated by Pi using GPT-5

Comment thread src/session-broker/brokerServer.ts Outdated
Comment on lines +115 to +116
/** Return whether one request carries browser-only headers but omits an Origin. */
function validateBrowserRequestOrigin(request: Request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Same "Return whether..." phrasing issue as validateFetchMetadata. The function rejects the request (returns a 403 Response) rather than returning a boolean predicate.

Suggested change
/** Return whether one request carries browser-only headers but omits an Origin. */
function validateBrowserRequestOrigin(request: Request) {
/** Block browser-looking requests that carry Sec-* headers but omit an Origin. */
function validateBrowserRequestOrigin(request: Request) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/session-broker/brokerServer.ts
Line: 115-116

Comment:
Same "Return whether..." phrasing issue as `validateFetchMetadata`. The function rejects the request (returns a 403 Response) rather than returning a boolean predicate.

```suggestion
/** Block browser-looking requests that carry Sec-* headers but omit an Origin. */
function validateBrowserRequestOrigin(request: Request) {
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated this comment as well so it describes the blocking behavior instead of implying a boolean predicate.

This comment was generated by Pi using GPT-5

Comment on lines 382 to +420
}
});

test("rejects browser Fetch Metadata for HTTP and websocket requests", async () => {
const port = await reserveLoopbackPort();
process.env.HUNK_MCP_HOST = "127.0.0.1";
process.env.HUNK_MCP_PORT = String(port);

const server = serveSessionBrokerDaemon();

try {
const response = await fetch(`http://127.0.0.1:${port}/health`, {
headers: { "sec-fetch-site": "cross-site" },
});
expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
error: "Browser request metadata is not allowed for the local session broker.",
});

const handshake = await readRawWebSocketHandshake(port, ["Sec-Fetch-Site: same-site"]);
expect(handshake).toStartWith("HTTP/1.1 403");
} finally {
server.stop(true);
}
});

test("rejects browser-looking requests that omit Origin", async () => {
const port = await reserveLoopbackPort();
process.env.HUNK_MCP_HOST = "127.0.0.1";
process.env.HUNK_MCP_PORT = String(port);

const server = serveSessionBrokerDaemon();

try {
const response = await fetch(`http://127.0.0.1:${port}/health`, {
headers: {
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing positive-case coverage for allowed Fetch-Metadata values

The test verifies that cross-site (HTTP) and same-site (WebSocket) are rejected, but there are no assertions that sec-fetch-site: none or sec-fetch-site: same-origin are explicitly allowed through validateFetchMetadata. Because sec-fetch-site: none is allowed by validateFetchMetadata but then caught by validateBrowserRequestOrigin (no Origin + Sec-* header present), the two functions interact subtly. A regression that accidentally tightens validateFetchMetadata to also block none would be invisible without a positive test exercising that path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/session-broker/brokerServer.test.ts
Line: 382-420

Comment:
**Missing positive-case coverage for allowed Fetch-Metadata values**

The test verifies that `cross-site` (HTTP) and `same-site` (WebSocket) are rejected, but there are no assertions that `sec-fetch-site: none` or `sec-fetch-site: same-origin` are explicitly allowed through `validateFetchMetadata`. Because `sec-fetch-site: none` is allowed by `validateFetchMetadata` but then caught by `validateBrowserRequestOrigin` (no Origin + Sec-* header present), the two functions interact subtly. A regression that accidentally tightens `validateFetchMetadata` to also block `none` would be invisible without a positive test exercising that path.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added positive coverage for both allowed Fetch Metadata values with a local Origin, so the test now verifies that same-origin and none pass through while cross-site/same-site remain blocked.

This comment was generated by Pi using GPT-5

@benvinegar benvinegar force-pushed the fix/session-browser-csrf-hardening branch from 045b024 to 40e0ced Compare May 19, 2026 19:16
@benvinegar
Copy link
Copy Markdown
Member Author

benvinegar commented May 19, 2026

Eh, this might be overkill. To be revisited.

@benvinegar benvinegar closed this May 19, 2026
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.

1 participant