HSTS is a hardcoded 2-year includeSubDomains preload commitment with no opt-out and no documentation of its value #271

Closed
opened 2026-08-24 02:11:58 +02:00 by clawbot · 5 comments
Collaborator

Found by a TLS deployment audit running real nginx in front of the app.

Every response carries Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, unconditionally and with no configuration option. The README names HSTS twice and never states the value.

An operator deploying at hooks.example.com therefore silently commits every subdomain of example.com to HTTPS-only for two years, and preload invites permanent inclusion in browser preload lists. That is a decision with consequences well beyond this service, made on the operator's behalf without telling them.

Verified it is also emitted over plaintext direct to the app (curl http://127.0.0.1:19001/pages/login returns the header). Browsers ignore HSTS over plaintext, so that part is harmless — noting it only for completeness.

Nothing in the app broke under HSTS during testing; this is about the commitment being invisible, not about it malfunctioning.

Not milestoned: the header is correct and desirable for the intended deployment, and the risk is limited to operators who own a parent domain with non-HTTPS subdomains.

Definition of done:

  • The max-age, includeSubDomains and preload values are documented in README where HSTS is mentioned, stating plainly that includeSubDomains affects the whole parent domain and that preload is effectively irreversible on browser timescales.
  • HSTS is configurable — at minimum the ability to drop includeSubDomains and preload, since those are the two that reach beyond this service. Keep the current values as the default; this is about an escape hatch, not a weakening.
  • If made configurable, the iron rule applies: a set-but-unparseable value must abort startup, never silently fall back.
Found by a TLS deployment audit running real nginx in front of the app. Every response carries `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`, unconditionally and with no configuration option. The README names HSTS twice and never states the value. An operator deploying at `hooks.example.com` therefore silently commits **every subdomain of `example.com`** to HTTPS-only for two years, and `preload` invites permanent inclusion in browser preload lists. That is a decision with consequences well beyond this service, made on the operator's behalf without telling them. Verified it is also emitted over plaintext direct to the app (`curl http://127.0.0.1:19001/pages/login` returns the header). Browsers ignore HSTS over plaintext, so that part is harmless — noting it only for completeness. Nothing in the app broke under HSTS during testing; this is about the commitment being invisible, not about it malfunctioning. Not milestoned: the header is correct and desirable for the intended deployment, and the risk is limited to operators who own a parent domain with non-HTTPS subdomains. Definition of done: - The `max-age`, `includeSubDomains` and `preload` values are documented in README where HSTS is mentioned, stating plainly that `includeSubDomains` affects the whole parent domain and that `preload` is effectively irreversible on browser timescales. - HSTS is configurable — at minimum the ability to drop `includeSubDomains` and `preload`, since those are the two that reach beyond this service. Keep the current values as the default; this is about an escape hatch, not a weakening. - If made configurable, the iron rule applies: a set-but-unparseable value must abort startup, never silently fall back.
Author
Collaborator

Question for the owner. The README now documents the header and its value ("HSTS is always sent, and is not configurable"), so the documentation half of this is done. What remains is whether the value keeps preload.

  • Keep as is: max-age=63072000; includeSubDomains; preload. Any host name this runs under, and every subdomain of it, is HTTPS-only for two years, and browsers may bake that into their preload lists permanently.
  • Drop preload (recommended): max-age=63072000; includeSubDomains. Still meets REPO_POLICIES.md (one year minimum, includeSubDomains), and stops the app from inviting a permanent, browser-side commitment on the operator's behalf. Preloading is a decision for whoever owns the domain, made at hstspreload.org, not by a service that happens to run under it.

An opt-out variable is not proposed: the policy says the header is always sent.

If you want it dropped, reply here and it becomes a one-line change plus the README sentence; if you want it kept, this issue closes as documented.

Model: fable-5-1

Question for the owner. The README now documents the header and its value ("HSTS is always sent, and is not configurable"), so the documentation half of this is done. What remains is whether the value keeps `preload`. - Keep as is: `max-age=63072000; includeSubDomains; preload`. Any host name this runs under, and every subdomain of it, is HTTPS-only for two years, and browsers may bake that into their preload lists permanently. - Drop `preload` (recommended): `max-age=63072000; includeSubDomains`. Still meets `REPO_POLICIES.md` (one year minimum, `includeSubDomains`), and stops the app from inviting a permanent, browser-side commitment on the operator's behalf. Preloading is a decision for whoever owns the domain, made at hstspreload.org, not by a service that happens to run under it. An opt-out variable is not proposed: the policy says the header is always sent. If you want it dropped, reply here and it becomes a one-line change plus the README sentence; if you want it kept, this issue closes as documented. Model: fable-5-1
sneak was assigned by clawbot 2026-09-21 09:20:32 +02:00
Owner

Make it a configurable option via environment variable, how long the preload setting is for, and make it default to 10 years. Allow the user to override that default with an environment variable. If they set it to zero, then disable the preload header.

Make it a configurable option via environment variable, how long the preload setting is for, and make it default to 10 years. Allow the user to override that default with an environment variable. If they set it to zero, then disable the preload header.
Author
Collaborator

Ruling (sneak, 2026-10-12): make the header's duration a setting. An environment variable sets how long it lasts, defaulting to 10 years; setting it to zero turns the preload header off. Reassigned to clawbot; webhooker builds it when its turn on sneak's priority list comes.

Model: opus-5-5

Ruling (sneak, 2026-10-12): make the header's duration a setting. An environment variable sets how long it lasts, defaulting to 10 years; setting it to zero turns the preload header off. Reassigned to clawbot; webhooker builds it when its turn on sneak's priority list comes. Model: opus-5-5
sneak was unassigned by clawbot 2026-10-13 00:07:18 +02:00
clawbot self-assigned this 2026-10-13 00:07:18 +02:00
Author
Collaborator

Plan, built from the ruling above:

  • New setting HSTS_MAX_AGE, a Go duration like SESSION_IDLE_TIMEOUT. Unset means 10 years (87600h), sent as Strict-Transport-Security: max-age=315360000; includeSubDomains; preload. includeSubDomains and preload stay as they are.
  • 0 turns the header off: no Strict-Transport-Security header is sent at all.
  • A set value that is negative, unparseable, or not a whole number of seconds stops startup with an error naming the variable.
  • /settings shows the value the server is running with.
  • README: the section "HSTS is always sent, and is not configurable" is rewritten for the setting, and the setting goes in the configuration list. It says plainly that includeSubDomains covers every subdomain, that preload invites permanent inclusion in browser preload lists, and that setting 0 later does not make a browser that already saw the header forget it.
  • Tests: unset gives the default header; 0 gives no header; a custom value gives that many seconds; bad values fail config loading.

Reading taken: "disable" means the header is not sent; max-age=0 is not sent.

Model: opus-5-5

Plan, built from the ruling above: - New setting `HSTS_MAX_AGE`, a Go duration like `SESSION_IDLE_TIMEOUT`. Unset means 10 years (`87600h`), sent as `Strict-Transport-Security: max-age=315360000; includeSubDomains; preload`. `includeSubDomains` and `preload` stay as they are. - `0` turns the header off: no `Strict-Transport-Security` header is sent at all. - A set value that is negative, unparseable, or not a whole number of seconds stops startup with an error naming the variable. - `/settings` shows the value the server is running with. - README: the section "HSTS is always sent, and is not configurable" is rewritten for the setting, and the setting goes in the configuration list. It says plainly that `includeSubDomains` covers every subdomain, that `preload` invites permanent inclusion in browser preload lists, and that setting `0` later does not make a browser that already saw the header forget it. - Tests: unset gives the default header; `0` gives no header; a custom value gives that many seconds; bad values fail config loading. Reading taken: "disable" means the header is not sent; `max-age=0` is not sent. Model: opus-5-5
Author
Collaborator

Built in #515, as planned above. Two additions to the plan: the value is also in the startup log line, and the README says includeSubDomains covers subdomains of the hostname that sends the header, not of its parent domain, so a deployment at hooks.example.com does not reach the rest of example.com as this issue's description assumed.

Model: opus-5-5

Built in https://git.eeqj.de/sneak/webhooker/pulls/515, as planned above. Two additions to the plan: the value is also in the startup log line, and the README says `includeSubDomains` covers subdomains of the hostname that sends the header, not of its parent domain, so a deployment at `hooks.example.com` does not reach the rest of `example.com` as this issue's description assumed. Model: opus-5-5
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#271