Files
runicgateway.com/SECURITY.md
wtclaude f2e59a2426
All checks were successful
PR checks / checks (pull_request) Successful in 9m46s
feat(delivery): phase 12 — the container, and the defect only a proxy could find
PLAN.md §13 phase 12, the last one. Four decisions of record, D54–D57, taking the
count to fifty-seven; recorded in §6, "How phase 12 delivered it".

A two-stage Dockerfile, a pull-only docker-compose.yml carrying both bind mounts,
.env.example, the workflow that publishes and deploys, CONTRIBUTING.md, the
community-health files this was the only repository of the ten to lack, and
DEPLOY.md.

D54 — a merge deploys, amending D6. build-image.yml pushes
runicgateway-site:latest and :sha-<7>, then rolls the container over on the
`rgcom` runner out of /opt/runicgateway.com, and waits for the container's own
healthcheck rather than for `up -d` to return.

D55 — the site runs on its own host behind a generic reverse proxy, so DEPLOY.md
states the four requirements rather than one worked example, and the container
binds 127.0.0.1 so the safe configuration is the default.

D56 — @astrojs/node derives the request protocol from req.socket.encrypted and
never reads x-forwarded-proto, so behind a TLS-terminating proxy the browser sends
Origin: https://… while the container computes http://… and Astro's CSRF check
compares them for equality. Every beta signup, from every visitor, was answered
403. serve.mjs now normalises both forwarded headers, unconditionally — the image
should deploy and work. Two assertions in test/headers.test.mjs hold both halves.

D57 — DEPLOY.md rather than a README section; SECURITY.md and CODE_OF_CONDUCT.md
are pointers to the org's copies rather than copies, because a copy would hard-code
the contact address D13 confines to brand.json.

Verified: npm run verify green (eleven checks, 36 unit tests, 7 served tests,
astro check 0 errors). The image was built and run with both mounts — a mounted
brand reached 51 files and all 50 search pages, /brand/* fell back per file, a
proxy-shaped signup reached the store, and the export CLI wrote both Play files to
the host mount. docker compose config caught a YAML trap in the healthcheck: a
block sequence reads the `: ` in `r.ok ? 0 : 1` as a mapping.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 16:54:38 -05:00

2.5 KiB

Security Policy

Please do not report security vulnerabilities through public issues, pull requests or the wiki. A public report tips off attackers before a fix is available.

Report privately, using the contact route in the organisation's security policy:

RunicGateway/docs → SECURITY.md

That document is the single copy for all ten repositories, and it carries the address, what to include in a report and what to expect back.

Why this file is a pointer rather than a copy

Every other repository in the organisation states the reporting address inline. This one does not, and the reason is a decision of record rather than an oversight: D13 (PLAN.md §5) confines the published contact address to brand.json, a bind-mounted file, so that changing it is a file copy and a container restart rather than a commit. scripts/checkFacts.mjs fails the build if an address appears anywhere in src/ or scripts/, and this file honours the same rule voluntarily — a hard-coded address in the repository root would be one more place to forget when the address moves.

What is worth reporting here

This site holds one thing of value and has one writing endpoint.

  • The closed-beta signup (/beta, PLAN.md §8) is the only route that writes. It stores an email address, a consent record and a salted hash of the IP address — never the address itself. Anything that lets a caller read rows, bypass the rate limit or the total cap, forge the signed form token, or recover an IP from a hash is in scope and worth reporting.
  • The branding mount (GET /brand/*, §7) reads files from a directory an operator controls. Path traversal out of that directory, or reaching a file type outside the route's allowlist, is in scope.
  • The Content-Security-Policy is a real response header written by scripts/serve.mjs. A page that is served no policy, or another page's policy, is a defect worth reporting — that exact bug has happened here once already (PLAN.md D48).

The site has no authenticated surface at all, by design: the tester list is managed from a shell against the bind mount, not from an admin page. There is no session, no cookie and no login to attack.

Vulnerabilities in the platform itself — the website, the sidecar, the shard plugin, the installer or the Android app — belong in the organisation's policy linked above, not here. This repository only describes them.