From 093a64215867d65f41740b32559bbbe2b2bd1fbd Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 2 Jul 2026 06:16:52 -0600 Subject: [PATCH 1/2] docs(security): document HTTP response-header hardening at the edge Add guidance for setting browser security-hardening headers (X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy, HSTS, Permissions-Policy, Cross-Origin-*) at a reverse proxy/CDN rather than at the Harper origin. Also covers the risk of shared-cache storing authenticated/allowRead-gated responses when Harper does not emit Cache-Control. Cross-linked from reference/rest/headers.md See Also section. Co-Authored-By: Claude Sonnet 4.6 --- reference/rest/headers.md | 1 + reference/security/configuration.md | 40 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 818398eb..13aaee16 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -95,3 +95,4 @@ This is not recommended for production use — prefer the `Accept` header for cl - [REST Overview](./overview.md) — HTTP methods and URL structure - [Content Types](./content-types.md) — Supported encoding formats - [Security Overview](../security/overview.md) — Authentication headers and mechanisms +- [HTTP Response Header Hardening](../security/configuration.md#http-response-header-hardening) — Setting browser security headers and cache-control at your edge diff --git a/reference/security/configuration.md b/reference/security/configuration.md index c34ad1d6..f029eb9c 100644 --- a/reference/security/configuration.md +++ b/reference/security/configuration.md @@ -62,6 +62,46 @@ Password hashing algorithm used when storing user passwords. Replaced the previo - **`sha256`** — Default. Good security and excellent performance. - **`argon2id`** — Highest security. More CPU-intensive; recommended for environments that do not require frequent password verifications. +## HTTP Response Header Hardening + +Harper is an origin/API server. By default it does not emit browser security-hardening response headers — `X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`, `Referrer-Policy`, `Strict-Transport-Security`, `Permissions-Policy`, or the `Cross-Origin-*` family (COOP, COEP, CORP). For browser-facing deployments these should be set at your reverse proxy, CDN, or edge layer, not at the origin. + +The highest-value, lowest-effort header to add is: + +```http +X-Content-Type-Options: nosniff +``` + +This prevents browsers from MIME-sniffing response bodies away from the declared `Content-Type`, which closes an entire class of content-injection attacks with essentially zero deployment cost. + +A typical Nginx snippet for the full hardening set: + +```nginx +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; +add_header Permissions-Policy "geolocation=(), camera=()" always; +# Cross-Origin isolation — only enable if your app explicitly needs it: +# add_header Cross-Origin-Opener-Policy "same-origin" always; +# add_header Cross-Origin-Embedder-Policy "require-corp" always; +``` + +CSP values are application-specific and intentionally omitted here — set them based on the origins your application legitimately loads scripts, styles, and media from. + +> These defaults may tighten in future Harper releases. Frame the edge configuration as a deployment baseline, not a permanent substitute for origin-level headers. + +### Caching of authenticated responses + +Harper does not emit `Cache-Control` headers on its responses. If you place Harper behind a shared cache or CDN, the cache must not store responses that depend on caller identity — authenticated reads, `allowRead`-gated rows, or any route that varies by `Authorization` header. + +Ensure the cache either: + +- **Excludes authenticated routes entirely** — pass `Cache-Control: no-store` at the edge for any path that requires an `Authorization` header, or +- **Keys the cache appropriately** — vary on `Authorization` (or on a session cookie) so each user's responses are stored and served separately. + +Failing to do this can cause one user's private data to be returned to another. This is a deployment configuration concern, not something Harper enforces at the origin today. + ## Related - [JWT Authentication](./jwt-authentication.md) From 3f41e7e3a87370d90155092e4f78e3936a522a3c Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 2 Jul 2026 06:35:06 -0600 Subject: [PATCH 2/2] docs(security): rework response-header hardening around app-level controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lead with the mechanism that works on Harper-hosted (Fabric) deployments: a custom Resource can set response headers via context.responseHeaders.set(). Clearly state the gap — no built-in/config way to apply headers to auto-generated REST/GraphQL endpoints and no global default (tracked in HarperFast/harper#1567) — and demote the reverse-proxy/CDN guidance to a self-hosted fallback. Split into a dedicated reference/security/response-headers.md page (added to the Reference sidebar), replacing the section previously wedged into the auth-config page. Cross-linked from the auth Configuration page and rest/headers.md. Co-Authored-By: Claude Sonnet 4.6 --- reference/rest/headers.md | 2 +- reference/security/configuration.md | 41 +------------- reference/security/response-headers.md | 75 ++++++++++++++++++++++++++ sidebarsReference.ts | 5 ++ 4 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 reference/security/response-headers.md diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 13aaee16..9ce21d81 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -95,4 +95,4 @@ This is not recommended for production use — prefer the `Accept` header for cl - [REST Overview](./overview.md) — HTTP methods and URL structure - [Content Types](./content-types.md) — Supported encoding formats - [Security Overview](../security/overview.md) — Authentication headers and mechanisms -- [HTTP Response Header Hardening](../security/configuration.md#http-response-header-hardening) — Setting browser security headers and cache-control at your edge +- [Response Header Hardening](../security/response-headers.md) — Adding browser security headers and cache-control to Harper responses (from a custom Resource, or at your own edge) diff --git a/reference/security/configuration.md b/reference/security/configuration.md index f029eb9c..c543b813 100644 --- a/reference/security/configuration.md +++ b/reference/security/configuration.md @@ -62,48 +62,9 @@ Password hashing algorithm used when storing user passwords. Replaced the previo - **`sha256`** — Default. Good security and excellent performance. - **`argon2id`** — Highest security. More CPU-intensive; recommended for environments that do not require frequent password verifications. -## HTTP Response Header Hardening - -Harper is an origin/API server. By default it does not emit browser security-hardening response headers — `X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`, `Referrer-Policy`, `Strict-Transport-Security`, `Permissions-Policy`, or the `Cross-Origin-*` family (COOP, COEP, CORP). For browser-facing deployments these should be set at your reverse proxy, CDN, or edge layer, not at the origin. - -The highest-value, lowest-effort header to add is: - -```http -X-Content-Type-Options: nosniff -``` - -This prevents browsers from MIME-sniffing response bodies away from the declared `Content-Type`, which closes an entire class of content-injection attacks with essentially zero deployment cost. - -A typical Nginx snippet for the full hardening set: - -```nginx -add_header X-Content-Type-Options "nosniff" always; -add_header X-Frame-Options "SAMEORIGIN" always; -add_header Referrer-Policy "strict-origin-when-cross-origin" always; -add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; -add_header Permissions-Policy "geolocation=(), camera=()" always; -# Cross-Origin isolation — only enable if your app explicitly needs it: -# add_header Cross-Origin-Opener-Policy "same-origin" always; -# add_header Cross-Origin-Embedder-Policy "require-corp" always; -``` - -CSP values are application-specific and intentionally omitted here — set them based on the origins your application legitimately loads scripts, styles, and media from. - -> These defaults may tighten in future Harper releases. Frame the edge configuration as a deployment baseline, not a permanent substitute for origin-level headers. - -### Caching of authenticated responses - -Harper does not emit `Cache-Control` headers on its responses. If you place Harper behind a shared cache or CDN, the cache must not store responses that depend on caller identity — authenticated reads, `allowRead`-gated rows, or any route that varies by `Authorization` header. - -Ensure the cache either: - -- **Excludes authenticated routes entirely** — pass `Cache-Control: no-store` at the edge for any path that requires an `Authorization` header, or -- **Keys the cache appropriately** — vary on `Authorization` (or on a session cookie) so each user's responses are stored and served separately. - -Failing to do this can cause one user's private data to be returned to another. This is a deployment configuration concern, not something Harper enforces at the origin today. - ## Related +- [Response Header Hardening](./response-headers.md) — Setting browser security headers (`X-Content-Type-Options`, CSP, etc.) and cache-control on Harper responses. - [JWT Authentication](./jwt-authentication.md) - [Basic Authentication](./basic-authentication.md) - [Users & Roles / Configuration](../users-and-roles/configuration.md) diff --git a/reference/security/response-headers.md b/reference/security/response-headers.md new file mode 100644 index 00000000..c755d3ca --- /dev/null +++ b/reference/security/response-headers.md @@ -0,0 +1,75 @@ +--- +id: response-headers +title: Response Header Hardening +--- + +Harper is an origin/API server, and by default it does not emit the browser security-hardening response headers that a public-facing web application typically wants — `X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`, `Referrer-Policy`, `Strict-Transport-Security`, `Permissions-Policy`, or the `Cross-Origin-*` family (COOP, COEP, CORP). Nor does it emit `Cache-Control`. For browser-facing deployments you generally want to add these. + +Where you add them depends on your deployment. On Harper-hosted deployments (Fabric) there is no customer-controlled reverse proxy, CDN, or edge to configure — so the reliable place to set these headers is from your application code, in a custom Resource. If you run your own edge, an edge configuration is also an option (see the fallback note below). + +## Setting headers from a custom Resource + +A custom [Resource](../resources/resource-api.md) handler can set response headers through the request context. When a Resource method is invoked over HTTP, `getContext()` returns the `Request` object, which carries a `responseHeaders` map with a `set(name, value)` method: + +```javascript +export class Product extends tables.Product { + get(query) { + const { responseHeaders } = this.getContext(); + // Highest-value, lowest-effort hardening header: + responseHeaders.set('X-Content-Type-Options', 'nosniff'); + // Frame-embedding and content-source policies go here as well. + // Keep these conservative and application-specific — e.g. only + // relax X-Frame-Options if your app is legitimately embedded: + responseHeaders.set('X-Frame-Options', 'SAMEORIGIN'); + // responseHeaders.set('Content-Security-Policy', "default-src 'self'"); + return super.get(query); + } +} +``` + +`X-Content-Type-Options: nosniff` is the highest-value, lowest-effort header to add: it stops browsers from MIME-sniffing a response away from its declared `Content-Type`, closing a class of content-injection attacks with essentially no cost. + +This applies to responses that flow through the custom Resource handler that sets them. It does not retroactively harden other endpoints — see the gap below. + +## The gap: auto-generated endpoints and global defaults + +There is currently no built-in or configuration-based way to apply these headers to Harper's auto-generated HTTP surfaces — the REST endpoints Harper exposes for a table, and the GraphQL endpoint — and no global response-header default that applies across all surfaces. The `responseHeaders` mechanism above only covers responses served by a custom Resource handler you have written. + +For now, security headers can be added from custom Resources; global and auto-endpoint coverage is tracked in [HarperFast/harper#1567](https://github.com/HarperFast/harper/issues/1567) ("Provide a built-in way to set security/cache response headers across all HTTP surfaces"). + +These defaults may tighten in a future release. Treat the guidance here as current behavior, not a permanent guarantee — check the release notes if you are relying on the absence of a header. + +## Caching of authenticated responses + +Harper does not emit `Cache-Control` on its responses. If a response is cached by anything shared between users — a CDN, a proxy cache, or a browser cache on a shared machine — a response that depends on the caller's identity must not be stored and replayed for a different caller. This includes authenticated reads and `allowRead`-gated rows, or any response that varies by `Authorization`. + +Until a built-in control lands (also tracked in [#1567](https://github.com/HarperFast/harper/issues/1567)), you can set `Cache-Control` from a custom Resource the same way as the hardening headers above: + +```javascript +get(query) { + const { responseHeaders } = this.getContext(); + // Prevent shared caches from storing an identity-dependent response: + responseHeaders.set('Cache-Control', 'no-store'); + return super.get(query); +} +``` + +If you do want identity-scoped responses cached, key the cache on identity (e.g. vary on `Authorization` or the session cookie) rather than storing a single shared copy. Getting this wrong can return one user's private data to another, so default to `no-store` for anything that depends on who is asking. + +## Fallback: setting headers at your own edge + +If you self-host Harper behind your own reverse proxy or CDN (not applicable to Harper-hosted/Fabric deployments), you can add the same headers there instead. This is the fallback rather than the primary answer, since the hosted deployment model has no such edge to configure. A minimal Nginx example: + +```nginx +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +``` + +CSP and `Strict-Transport-Security` values are application-specific; set them based on the origins your application legitimately loads from and your HTTPS posture. + +## Related + +- [Resource API](../resources/resource-api.md) — Custom Resource handlers and the request context +- [REST Headers](../rest/headers.md) — Standard request/response headers Harper does emit +- [Security Overview](./overview.md) diff --git a/sidebarsReference.ts b/sidebarsReference.ts index 08ecfdc5..a7189e55 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -285,6 +285,11 @@ const sidebars: SidebarsConfig = { id: 'security/configuration', label: 'Configuration', }, + { + type: 'doc', + id: 'security/response-headers', + label: 'Response Header Hardening', + }, { type: 'doc', id: 'security/basic-authentication',