aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md6
-rw-r--r--README.md30
2 files changed, 30 insertions, 6 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 5ffd8ba..5aec663 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -154,7 +154,11 @@ internal/web/ server.go (host router, middleware, render helpers)
as well as to `reservedPageSlugs`) also get `X-Frame-Options: DENY` and
a CSP of `frame-ancestors`/`base-uri`/`form-action`/`object-src` only —
no `script-src`, the dashboard's inline scripts are a product
- constraint. Public blog pages carry no framing rule (owner content). Flood
+ constraint. Public blog pages carry no framing rule (owner content).
+ Audited and left as is (2026-09): SQL is all parameterised (`likeEscape`
+ for ILIKE, `pgx.Identifier` for database names); templates are
+ `html/template` and the only `{{html}}` sinks are owner-authored content;
+ `/media` never renders a type a browser would script (`filetype.go`). Flood
control for everything else stays at the reverse proxy (`limit_req`).
- **Templates**: each page file is parsed together with its layout
(`layouts/dashboard.html` or `layouts/blog.html` for `blog/*`) and all
diff --git a/README.md b/README.md
index ba79190..84980bd 100644
--- a/README.md
+++ b/README.md
@@ -112,19 +112,29 @@ with the original `Host` header. Keep `HTTPS=true` and `TRUST_PROXY=true` in
nginx example:
```nginx
+limit_req_zone $binary_remote_addr zone=blogspace:10m rate=10r/s; # per client; the app throttles login and search itself
+
server {
listen 443 ssl;
server_name example.com *.example.com; # wildcard certificate
client_max_body_size 101m; # the Files page sends up to 10 files per request: >= 10 x the largest blog limit + 1 MB
+ limit_req zone=blogspace burst=20 nodelay;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr; # exactly one address: TRUST_PROXY reads the last one
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
+The app never sees TLS itself, so tell it: `HTTPS=true` (https links, `Secure`
+cookie, HSTS for the whole domain) and `TRUST_PROXY=true` (client addresses from
+`X-Forwarded-For`). Flood control for the site as a whole belongs to the proxy
+(`limit_req` above); the app throttles the two anonymous endpoints worth
+abusing on its own: login attempts (10, then 10 a minute, per address and per
+account) and blog search (30, then 30 a minute, per address).
+
Caddy: `example.com, *.example.com { reverse_proxy 127.0.0.1:8080 }` (wildcard
certificates need the DNS challenge).
@@ -195,7 +205,17 @@ shadowed by management routes (`webadmin`, `admin`, `b`, …) are rejected for e
### Auth notes
-Sessions are HS256 JWTs in an `HttpOnly` cookie. The token carries the user's
-`token_version`; changing a password or disabling a user bumps it, which logs
-out every existing session. Every POST carries a `_csrf` field derived from the
-same secret, so old browsers without `SameSite` support are protected too.
+Sessions are HS256 JWTs in an `HttpOnly`, `SameSite=Lax` cookie (`Secure` with
+`HTTPS=true`). The token carries the user's `token_version`; changing a
+password or disabling a user bumps it, which logs out every existing session.
+Every POST carries a `_csrf` field derived from the same secret, so old browsers
+without `SameSite` support are protected too, and a POST a modern browser marks
+as coming from another origin (`Sec-Fetch-Site`) is refused outright — a blog
+on a subdomain counts as another origin. Failed logins and throttled requests
+are logged with the client address (fail2ban-friendly). Management pages send
+`X-Frame-Options: DENY` and a CSP that keeps their forms on this origin; every
+response says `X-Content-Type-Options: nosniff`.
+
+Content bloggers write in HTML mode and the custom HTML module are published
+unsanitised on the blog's own origin, by design; the dashboard origin never
+renders them (HTML previews run in a sandboxed frame).