A page on another origin of the same site can post as the signed-in user #271

Open
opened 2026-11-10 23:08:35 +01:00 by clawbot · 1 comment
Collaborator

A page on another origin of the same site (another port, or another subdomain of the server's domain) can act as the signed-in user. It sends, say, POST /api/v1/messages with Content-Type: text/plain and credentials: "include". The browser sends that without a preflight and attaches the SameSite=Strict cookie, since the site is the same, and the handler decodes the body without looking at its type, so the message goes out as the user. A plain HTML form can do the same with enctype="text/plain", and a cross-origin POST /api/v1/logout signs the user out. Found in the review of #269.

A request with Content-Type: application/json is not "simple": a browser preflights it, and the preflight fails for a credentialed request because the server never answers with Access-Control-Allow-Credentials (#261). So refusing every other type closes this.

Definition of done

  • A middleware in internal/middleware, used on the /api/v1 router in internal/server/routes.go: a POST, PUT or DELETE request whose Content-Type media type is not application/json (parameters such as charset allowed; parse with mime.ParseMediaType) gets 415 with the API's usual JSON error body, before the handler runs. A missing Content-Type is refused too.
  • The web client (web/) and neoirc-cli (internal/cli/api) send Content-Type: application/json on every such request, POST /api/v1/logout included.
  • README.md "Using the HTTP API": one convention bullet stating the rule and the 415, and every curl example for those requests carries the header.
  • Tests, table-driven, external test packages: text/plain, application/x-www-form-urlencoded, multipart/form-data and a missing type each get 415 and change nothing (no message stored, no logout); application/json and application/json; charset=utf-8 pass. A GET is unaffected.
  • The web client keeps working: load it in a headless browser in a container and use it (sign in, join, send, log out) before pushing.

Start after #261 is merged into next, since both change internal/middleware and internal/server/routes.go.

Read first: AGENTS.md, CONVENTIONS.md, REPO_POLICIES.md.

Model: opus-5-5

A page on another origin of the same site (another port, or another subdomain of the server's domain) can act as the signed-in user. It sends, say, `POST /api/v1/messages` with `Content-Type: text/plain` and `credentials: "include"`. The browser sends that without a preflight and attaches the `SameSite=Strict` cookie, since the site is the same, and the handler decodes the body without looking at its type, so the message goes out as the user. A plain HTML form can do the same with `enctype="text/plain"`, and a cross-origin `POST /api/v1/logout` signs the user out. Found in the review of https://git.eeqj.de/sneak/neoirc/pulls/269. A request with `Content-Type: application/json` is not "simple": a browser preflights it, and the preflight fails for a credentialed request because the server never answers with `Access-Control-Allow-Credentials` (https://git.eeqj.de/sneak/neoirc/issues/261). So refusing every other type closes this. ## Definition of done - A middleware in `internal/middleware`, used on the `/api/v1` router in `internal/server/routes.go`: a `POST`, `PUT` or `DELETE` request whose `Content-Type` media type is not `application/json` (parameters such as `charset` allowed; parse with `mime.ParseMediaType`) gets `415` with the API's usual JSON error body, before the handler runs. A missing `Content-Type` is refused too. - The web client (`web/`) and `neoirc-cli` (`internal/cli/api`) send `Content-Type: application/json` on every such request, `POST /api/v1/logout` included. - `README.md` "Using the HTTP API": one convention bullet stating the rule and the `415`, and every `curl` example for those requests carries the header. - Tests, table-driven, external test packages: `text/plain`, `application/x-www-form-urlencoded`, `multipart/form-data` and a missing type each get `415` and change nothing (no message stored, no logout); `application/json` and `application/json; charset=utf-8` pass. A `GET` is unaffected. - The web client keeps working: load it in a headless browser in a container and use it (sign in, join, send, log out) before pushing. Start after https://git.eeqj.de/sneak/neoirc/issues/261 is merged into `next`, since both change `internal/middleware` and `internal/server/routes.go`. Read first: `AGENTS.md`, `CONVENTIONS.md`, `REPO_POLICIES.md`. Model: opus-5-5
clawbot self-assigned this 2026-11-10 23:08:35 +01:00
Author
Collaborator

Built in #273. Every POST, PUT and DELETE under /api/v1/ now gets 415 unless its Content-Type is application/json. The web client and neoirc-cli already sent that header, so neither changed. In a headless browser the web client still signs in, joins, sends and quits, and a text/plain POST from the page gets 415.

Model: opus-5-5

Built in https://git.eeqj.de/sneak/neoirc/pulls/273. Every `POST`, `PUT` and `DELETE` under `/api/v1/` now gets `415` unless its `Content-Type` is `application/json`. The web client and `neoirc-cli` already sent that header, so neither changed. In a headless browser the web client still signs in, joins, sends and quits, and a `text/plain` `POST` from the page gets `415`. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/neoirc#271