Another origin's page can store reports through its visitors' browsers #126

Closed
opened 2026-10-14 11:01:06 +02:00 by clawbot · 1 comment
Collaborator

With CORS_ALLOWED_ORIGINS empty the server sends no CORS headers, but POST /api/v1/reports stores any JSON body whatever its Content-Type. A page on another origin can send a report as Content-Type: text/plain, which browsers send without asking the server first (no CORS preflight). The browser hides the answer from that page, but the report is stored. So any site can make its visitors' browsers post made-up reports, each visitor's address with its own REPORTS_PER_MINUTE allowance, and the oldest report files are deleted to make room for them.

Where, on next at b1eefe0: backend/internal/handlers/report.go decodes the body at line 42 without looking at Content-Type; backend/internal/middleware/middleware.go, CORS (line 337) adds nothing when no origin is listed (line 340).

How to see it: post {"clientId":"x","hosts":[]} to /api/v1/reports with Content-Type: text/plain;charset=UTF-8 and Origin: https://other.example. The answer is 200 with no Access-Control-Allow-Origin, and the report is stored.

What is right: backend/README.md, CORS: "by default the server sends no CORS headers, and browsers let no other origin's pages call it". #20 removed the wildcard CORS so that only the page's own origin, or a listed one, uses the API.

Severity: low. Anyone can post reports directly anyway; this lets a busy site do it from its visitors' addresses, past the per-address limit.

Definition of done

  • A report that another origin's page can send without a preflight, such as one with Content-Type: text/plain, is refused and not stored; the page's own reports, sent as application/json, are stored as before.
  • A test posts a report as text/plain and finds it refused, and one as application/json and finds it stored.
  • TODO.md updated in the same commit; commit title ends (closes #N).

Model: opus-5-5

With `CORS_ALLOWED_ORIGINS` empty the server sends no CORS headers, but `POST /api/v1/reports` stores any JSON body whatever its `Content-Type`. A page on another origin can send a report as `Content-Type: text/plain`, which browsers send without asking the server first (no CORS preflight). The browser hides the answer from that page, but the report is stored. So any site can make its visitors' browsers post made-up reports, each visitor's address with its own `REPORTS_PER_MINUTE` allowance, and the oldest report files are deleted to make room for them. **Where**, on `next` at `b1eefe0`: `backend/internal/handlers/report.go` decodes the body at line 42 without looking at `Content-Type`; `backend/internal/middleware/middleware.go`, `CORS` (line 337) adds nothing when no origin is listed (line 340). **How to see it**: post `{"clientId":"x","hosts":[]}` to `/api/v1/reports` with `Content-Type: text/plain;charset=UTF-8` and `Origin: https://other.example`. The answer is 200 with no `Access-Control-Allow-Origin`, and the report is stored. **What is right**: `backend/README.md`, CORS: "by default the server sends no CORS headers, and browsers let no other origin's pages call it". https://git.eeqj.de/sneak/netwatch/issues/20 removed the wildcard CORS so that only the page's own origin, or a listed one, uses the API. **Severity**: low. Anyone can post reports directly anyway; this lets a busy site do it from its visitors' addresses, past the per-address limit. ## Definition of done - [ ] A report that another origin's page can send without a preflight, such as one with `Content-Type: text/plain`, is refused and not stored; the page's own reports, sent as `application/json`, are stored as before. - [ ] A test posts a report as `text/plain` and finds it refused, and one as `application/json` and finds it stored. - [ ] `TODO.md` updated in the same commit; commit title ends ` (closes #N)`. Model: opus-5-5
clawbot self-assigned this 2026-10-14 11:01:06 +02:00
Author
Collaborator

#132: POST /api/v1/reports refuses a report whose Content-Type is not application/json (parameters such as charset allowed) with 415 and the usual error body, storing nothing. A request with no Content-Type is refused too. backend/README.md, Report limits, says so.

Model: opus-5-5

https://git.eeqj.de/sneak/netwatch/pulls/132: `POST /api/v1/reports` refuses a report whose `Content-Type` is not `application/json` (parameters such as `charset` allowed) with 415 and the usual error body, storing nothing. A request with no `Content-Type` is refused too. `backend/README.md`, Report limits, says so. 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/netwatch#126