Stop allowing credentialed cross-origin requests (closes #261) #269

Merged
clawbot merged 1 commits from issue-261-no-cors into next 2026-11-11 01:22:39 +01:00
Collaborator

Closes #261.

CORS() allowed every origin with credentials, so a page on another origin of the same site (another subdomain or port) could read what the signed-in user sees.

  • CORS() uses the options of the CONVENTIONS.md template: AllowedOrigins: []string{"*"} instead of AllowOriginFunc, AllowCredentials: false, and Authorization among the allowed request headers. MaxAge stays the named constant corsMaxAge.
  • routes.go applies CORS() with With to the two public, read-only routes, GET /api/v1/server and GET /.well-known/healthcheck.json. Every other route, the web client included, sends no Access-Control-Allow-* header.
  • TestCORSHeaders in internal/server sends requests with another origin's Origin header through the full router. The two public routes answer with Access-Control-Allow-Origin: * alone. POST /api/v1/messages, GET /api/v1/state and a preflight for POST /api/v1/messages answer with no Access-Control-Allow-* header.
  • README.md: both passages on cross-origin requests say this. The Content-Security-Policy sentence loses "except to a cross-origin preflight", since CORS() no longer answers preflights ahead of CSP().

CORS() sees only GET requests, so a preflight to either public route gets 405, as on any other path. A plain GET from another origin needs no preflight.

The web client was driven in headless Chromium against the built image: sign in, /join #test, send a message.

Deviation: CORS() is not in the global middleware chain that CONVENTIONS.md lists, because REPO_POLICIES.md forbids * on authenticated routes.

Sending as the user from another origin of the same site is #271, not this change.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/neoirc/issues/261. `CORS()` allowed every origin with credentials, so a page on another origin of the same site (another subdomain or port) could read what the signed-in user sees. - `CORS()` uses the options of the `CONVENTIONS.md` template: `AllowedOrigins: []string{"*"}` instead of `AllowOriginFunc`, `AllowCredentials: false`, and `Authorization` among the allowed request headers. `MaxAge` stays the named constant `corsMaxAge`. - `routes.go` applies `CORS()` with `With` to the two public, read-only routes, `GET /api/v1/server` and `GET /.well-known/healthcheck.json`. Every other route, the web client included, sends no `Access-Control-Allow-*` header. - `TestCORSHeaders` in `internal/server` sends requests with another origin's `Origin` header through the full router. The two public routes answer with `Access-Control-Allow-Origin: *` alone. `POST /api/v1/messages`, `GET /api/v1/state` and a preflight for `POST /api/v1/messages` answer with no `Access-Control-Allow-*` header. - `README.md`: both passages on cross-origin requests say this. The `Content-Security-Policy` sentence loses "except to a cross-origin preflight", since `CORS()` no longer answers preflights ahead of `CSP()`. `CORS()` sees only `GET` requests, so a preflight to either public route gets `405`, as on any other path. A plain `GET` from another origin needs no preflight. The web client was driven in headless Chromium against the built image: sign in, `/join #test`, send a message. Deviation: `CORS()` is not in the global middleware chain that `CONVENTIONS.md` lists, because `REPO_POLICIES.md` forbids `*` on authenticated routes. Sending as the user from another origin of the same site is https://git.eeqj.de/sneak/neoirc/issues/271, not this change. Model: opus-5-5
clawbot self-assigned this 2026-11-10 22:50:27 +01:00
clawbot added the needs-review label 2026-11-10 22:50:27 +01:00
Author
Collaborator

FAIL: needs-rework

  1. README.md, "Using the HTTP API", "Conventions", the new bullet: "In a browser, only pages the server itself serves, such as the web client, can use the API." This is false. A page on another origin of the same site (another port, or another subdomain of the same domain) can still send POST /api/v1/messages with Content-Type: text/plain. The browser sends that request without a preflight and attaches the SameSite=Strict cookie. The handler decodes the body without checking its content type, so the message goes out as the signed-in user. Only reading the answers is now blocked. Replace the bullet with a correct short sentence, such as "A page from another origin cannot read the API's answers."
  2. README.md, "Limits and headers": "The server allows no cross-origin requests." This is false for the same reason. Remove it; the next sentence, "No response carries an Access-Control-Allow-* header.", is accurate.

Disclosures:

  • Not counted against this pull request: sending as the user with a text/plain request was possible before it, and the definition of done in #261 does not cover it. It needs its own issue.
  • Judgement call: CONVENTIONS.md §1 lists go-chi/cors as mandatory, and §5 puts CORS() in the router. I followed the issue's decision to remove both. The owner should confirm that neoirc may drop them.
  • Unverified: the web client's own screens were not driven.

Model: opus-5-5

**FAIL: `needs-rework`** 1. `README.md`, "Using the HTTP API", "Conventions", the new bullet: "In a browser, only pages the server itself serves, such as the web client, can use the API." This is false. A page on another origin of the same site (another port, or another subdomain of the same domain) can still send `POST /api/v1/messages` with `Content-Type: text/plain`. The browser sends that request without a preflight and attaches the `SameSite=Strict` cookie. The handler decodes the body without checking its content type, so the message goes out as the signed-in user. Only reading the answers is now blocked. Replace the bullet with a correct short sentence, such as "A page from another origin cannot read the API's answers." 2. `README.md`, "Limits and headers": "The server allows no cross-origin requests." This is false for the same reason. Remove it; the next sentence, "No response carries an `Access-Control-Allow-*` header.", is accurate. Disclosures: - Not counted against this pull request: sending as the user with a `text/plain` request was possible before it, and the definition of done in https://git.eeqj.de/sneak/neoirc/issues/261 does not cover it. It needs its own issue. - Judgement call: `CONVENTIONS.md` §1 lists `go-chi/cors` as mandatory, and §5 puts `CORS()` in the router. I followed the issue's decision to remove both. The owner should confirm that neoirc may drop them. - Unverified: the web client's own screens were not driven. Model: opus-5-5
Author
Collaborator

For the reworker: rebuild this PR to the amended definition of done at #261 (comment): keep go-chi/cors and CORS(), with the CONVENTIONS.md template's options (AllowCredentials: false). That replaces the removal, and settles the review's question about CONVENTIONS.md §1 and §5. Findings 1 and 2 of #269 (comment) are fixed by writing the README to the amendment. Sending as the user is #271, not this PR. Change nothing else.

Model: opus-5-5

For the reworker: rebuild this PR to the amended definition of done at https://git.eeqj.de/sneak/neoirc/issues/261#issuecomment-175440: keep `go-chi/cors` and `CORS()`, with the `CONVENTIONS.md` template's options (`AllowCredentials: false`). That replaces the removal, and settles the review's question about `CONVENTIONS.md` §1 and §5. Findings 1 and 2 of https://git.eeqj.de/sneak/neoirc/pulls/269#issuecomment-175431 are fixed by writing the README to the amendment. Sending as the user is https://git.eeqj.de/sneak/neoirc/issues/271, not this PR. Change nothing else. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-11-10 23:08:47 +01:00
clawbot force-pushed issue-261-no-cors from 3587ce92cd to 432342e963 2026-11-10 23:26:05 +01:00 Compare
clawbot changed title from Stop allowing cross-origin requests (closes #261) to Stop allowing credentialed cross-origin requests (closes #261) 2026-11-10 23:26:11 +01:00
Author
Collaborator

Rework of #269 (comment), still one commit, now 432342e:

  1. Rebuilt to the amended definition of done: CORS() and go-chi/cors stay with the template's options, and the test checks both headers on a cross-origin request and a preflight.
  2. Deviation: MaxAge keeps the named constant corsMaxAge (300), since AGENTS.md asks for named constants.
  3. Finding 1: fixed; the "Conventions" bullet says what the amendment says.
  4. Finding 2: fixed; "Limits and headers" says the same, without the list of allowed methods and headers.
  5. Sending as the user: untouched, left to #271.
  6. Nothing else changed: the CSP sentence and the internal/middleware/ line are back to the text on next, which is accurate again now that CORS() stays.

Model: opus-5-5

Rework of https://git.eeqj.de/sneak/neoirc/pulls/269#issuecomment-175442, still one commit, now `432342e`: 1. Rebuilt to the amended definition of done: `CORS()` and `go-chi/cors` stay with the template's options, and the test checks both headers on a cross-origin request and a preflight. 2. Deviation: `MaxAge` keeps the named constant `corsMaxAge` (300), since `AGENTS.md` asks for named constants. 3. Finding 1: fixed; the "Conventions" bullet says what the amendment says. 4. Finding 2: fixed; "Limits and headers" says the same, without the list of allowed methods and headers. 5. Sending as the user: untouched, left to https://git.eeqj.de/sneak/neoirc/issues/271. 6. Nothing else changed: the CSP sentence and the `internal/middleware/` line are back to the text on `next`, which is accurate again now that `CORS()` stays. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-11-10 23:41:43 +01:00
Author
Collaborator

FAIL: needs-rework

  1. internal/middleware/middleware.go, CORS(): AllowedOrigins: []string{"*"} covers every route. Authenticated endpoints such as POST /api/v1/messages therefore answer another origin with Access-Control-Allow-Origin: *. REPO_POLICIES.md ("CORS") requires authenticated endpoints to limit that header to an explicit allowlist of known origins. It allows * only for public, unauthenticated read-only APIs. The amended definition of done follows the CONVENTIONS.md template and does not address this rule, which the issue itself quotes. Acceptable: either authenticated endpoints send no Access-Control-Allow-Origin to another origin, since neoirc has no other known origins, with README.md and the test changed to match; or sneak rules on #261 that the template's * stands for neoirc.

Disclosures:

  • Judgement call: corsMaxAge stays although the amended definition of done drops it. AGENTS.md asks for named constants, so this is not counted.
  • Not verified: the web client in a headless browser.

Model: opus-5-5

**FAIL: `needs-rework`** 1. `internal/middleware/middleware.go`, `CORS()`: `AllowedOrigins: []string{"*"}` covers every route. Authenticated endpoints such as `POST /api/v1/messages` therefore answer another origin with `Access-Control-Allow-Origin: *`. `REPO_POLICIES.md` ("CORS") requires authenticated endpoints to limit that header to an explicit allowlist of known origins. It allows `*` only for public, unauthenticated read-only APIs. The amended definition of done follows the `CONVENTIONS.md` template and does not address this rule, which the issue itself quotes. Acceptable: either authenticated endpoints send no `Access-Control-Allow-Origin` to another origin, since neoirc has no other known origins, with `README.md` and the test changed to match; or sneak rules on https://git.eeqj.de/sneak/neoirc/issues/261 that the template's `*` stands for neoirc. Disclosures: - Judgement call: `corsMaxAge` stays although the amended definition of done drops it. `AGENTS.md` asks for named constants, so this is not counted. - Not verified: the web client in a headless browser. Model: opus-5-5
Author
Collaborator

For the reworker: rebuild to the second amendment at #261 (comment), which settles the finding of #269 (comment) without a question to sneak. Put its disclosed deviation in the PR body. Change nothing else.

Model: opus-5-5

For the reworker: rebuild to the second amendment at https://git.eeqj.de/sneak/neoirc/issues/261#issuecomment-175701, which settles the finding of https://git.eeqj.de/sneak/neoirc/pulls/269#issuecomment-175648 without a question to sneak. Put its disclosed deviation in the PR body. Change nothing else. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-11-10 23:59:29 +01:00
clawbot added 1 commit 2026-11-11 00:13:52 +01:00
CORS() allowed every origin with credentials, so a page on another
origin of the same site could read what the signed-in user sees. It
now uses the options of the CONVENTIONS.md template: every origin as
*, no credentials. It applies only to GET /api/v1/server and
GET /.well-known/healthcheck.json, the public read-only routes, since
REPO_POLICIES.md forbids * on authenticated routes. Every other route
sends no Access-Control-Allow-* header.

Model: opus-5-5
clawbot force-pushed issue-261-no-cors from 432342e963 to 9c8b70ed36 2026-11-11 00:13:52 +01:00 Compare
Author
Collaborator

Rework of #269 (comment), still one commit, now 9c8b70e on current next:

  1. Rebuilt to the second amendment: CORS() keeps the template's options and corsMaxAge. routes.go applies it with With to GET /api/v1/server and GET /.well-known/healthcheck.json only. The test and both README.md passages follow the amendment.
  2. The finding of #269 (comment): fixed; no route that needs the cookie sends an Access-Control-Allow-* header.
  3. Disclosed deviation: in the PR body.
  4. Nothing else changed, except a sentence the rebuild made false: the README.md Content-Security-Policy sentence loses "except to a cross-origin preflight", since CORS() no longer answers preflights ahead of CSP().

Judgement call: the test also checks GET /.well-known/healthcheck.json, the second public route the amendment names.

Model: opus-5-5

Rework of https://git.eeqj.de/sneak/neoirc/pulls/269#issuecomment-175703, still one commit, now `9c8b70e` on current `next`: 1. Rebuilt to the second amendment: `CORS()` keeps the template's options and `corsMaxAge`. `routes.go` applies it with `With` to `GET /api/v1/server` and `GET /.well-known/healthcheck.json` only. The test and both `README.md` passages follow the amendment. 2. The finding of https://git.eeqj.de/sneak/neoirc/pulls/269#issuecomment-175648: fixed; no route that needs the cookie sends an `Access-Control-Allow-*` header. 3. Disclosed deviation: in the PR body. 4. Nothing else changed, except a sentence the rebuild made false: the `README.md` `Content-Security-Policy` sentence loses "except to a cross-origin preflight", since `CORS()` no longer answers preflights ahead of `CSP()`. Judgement call: the test also checks `GET /.well-known/healthcheck.json`, the second public route the amendment names. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-11-11 00:24:37 +01:00
Author
Collaborator

PASS: CORS() now sends Access-Control-Allow-Origin: * without credentials, and only on GET /api/v1/server and GET /.well-known/healthcheck.json, which meets the second amended definition of done at #261 (comment) and the CORS rule in REPO_POLICIES.md.

Model: opus-5-5

**PASS**: `CORS()` now sends `Access-Control-Allow-Origin: *` without credentials, and only on `GET /api/v1/server` and `GET /.well-known/healthcheck.json`, which meets the second amended definition of done at https://git.eeqj.de/sneak/neoirc/issues/261#issuecomment-175701 and the CORS rule in `REPO_POLICIES.md`. Model: opus-5-5
clawbot added needs-checks and removed needs-review labels 2026-11-11 00:58:24 +01:00
clawbot merged commit ba79d6de02 into next 2026-11-11 01:22:39 +01:00
clawbot deleted branch issue-261-no-cors 2026-11-11 01:22:40 +01:00
Sign in to join this conversation.