Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/response-header-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cronn/playwright-utils": minor
---

Add `responseHeaderReport` to evaluate response headers against recommended security headers for HTML documents or API responses and render the result as a Markdown report for file snapshots.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Response Header Report

## Detected Headers

None

## Recommendations

- `cache-control`: Missing, add header
- `content-security-policy`: Missing, add header
- `content-type`: Missing, add header
- `cross-origin-opener-policy`: Missing, add `same-origin`
- `cross-origin-resource-policy`: Missing, add `same-origin`
- `strict-transport-security`: Missing, add `max-age=31536000; includeSubDomains`
- `x-content-type-options`: Missing, add `nosniff`
- `x-robots-tag`: Missing, add header
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'none'; frame-ancestors 'none'` |
| `content-type` | `application/json` |
| `cross-origin-opener-policy` | `unsafe-none` |
| `cross-origin-resource-policy` | `cross-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `none` |
| `x-robots-tag` | `noindex` |

## Recommendations

- `cross-origin-opener-policy`: Change value to `same-origin`
- `cross-origin-resource-policy`: Change value to `same-origin`
- `x-content-type-options`: Change value to `nosniff`
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'none'; frame-ancestors 'none'` |
| `content-type` | `application/json` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |

## Recommendations

None
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'none'; frame-ancestors 'none'` |
| `content-type` | `application/json` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `access-control-allow-origin` | `*` |

## Recommendations

- `access-control-allow-origin`: Remove header
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'none'; frame-ancestors 'none'` |
| `content-type` | `application/json` |
| `cross-origin-opener-policy` | ` Same-Origin ` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `MAX-AGE=31536000 ;includesubdomains;` |
| `x-content-type-options` | `NoSniff` |
| `x-robots-tag` | `noindex` |

## Recommendations

None
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | ------------------------------------------------------ |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age="63072000"; includeSubDomains; preload` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=(), microphone=(), geolocation=(), payment=()` |
| `referrer-policy` | `no-referrer` |
| `x-frame-options` | `DENY` |

## Recommendations

None
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | ------------------------------------------------------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'; script-src 'nonce-[NONCE_0]'; style-src 'nonce-[NONCE_0]'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=(), microphone=(), geolocation=()` |
| `referrer-policy` | `no-referrer` |
| `x-frame-options` | `DENY` |

## Recommendations

None
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Response Header Report

## Detected Headers

None

## Recommendations

- `cache-control`: Missing, add header
- `content-security-policy`: Missing, add header
- `content-type`: Missing, add header
- `cross-origin-opener-policy`: Missing, add `same-origin`
- `cross-origin-resource-policy`: Missing, add `same-origin`
- `strict-transport-security`: Missing, add `max-age=31536000; includeSubDomains`
- `x-content-type-options`: Missing, add `nosniff`
- `x-robots-tag`: Missing, add header
- `permissions-policy`: Missing, add `geolocation=(), microphone=(), camera=()`
- `referrer-policy`: Missing, add `no-referrer`
- `x-frame-options`: Missing, add `DENY`
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `unsafe-none` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=3600` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=()` |
| `referrer-policy` | `unsafe-url` |
| `x-frame-options` | `ALLOW-FROM https://example.com` |

## Recommendations

- `cross-origin-opener-policy`: Change value to `same-origin`
- `strict-transport-security`: Change value to `max-age=31536000; includeSubDomains`
- `permissions-policy`: Change value to `geolocation=(), microphone=(), camera=()`
- `referrer-policy`: Change value to `no-referrer`
- `x-frame-options`: Change value to `DENY`
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | ------------------------------------------ |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=(), microphone=(), geolocation=()` |
| `referrer-policy` | `no-referrer` |
| `x-frame-options` | `DENY` |

## Recommendations

None
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | ------------------------------------------ |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `same-origin` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=31536000; includeSubDomains` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=(), microphone=(), geolocation=()` |
| `referrer-policy` | `no-referrer` |
| `x-frame-options` | `DENY` |
| `access-control-allow-origin` | `*` |
| `x-xss-protection` | `1; mode=block` |

## Recommendations

- `access-control-allow-origin`: Remove header
- `x-xss-protection`: Remove header
8 changes: 7 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ export default defineConfig({
},
{
text: "Security",
items: [{ text: "CSP Report", link: "/security/csp-report" }],
items: [
{ text: "CSP Report", link: "/security/csp-report" },
{
text: "Response Header Report",
link: "/security/response-header-report",
},
],
},
{
text: "Snapshot Testing",
Expand Down
71 changes: 71 additions & 0 deletions docs/src/security/response-header-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Response Header Report

Security headers protect against common attacks like clickjacking, MIME sniffing or protocol downgrades, but a missing or weakened header usually goes unnoticed because the application keeps working. `responseHeaderReport` evaluates the headers of a response against a set of recommended security headers and returns a Markdown report, which can be stored as a file snapshot to detect any change to the headers.

## Usage

```ts
import {
getResponseHeaders,
responseHeaderReport,
} from "@cronn/playwright-utils";
import { expect, test } from "@playwright/test";

test("has secure response headers", async ({ page }) => {
const response = await page.goto("/");
const headers = await getResponseHeaders(response);

expect(responseHeaderReport(headers, { type: "document" })).toMatchTextFile({
fileExtension: "md",
});
});

test("has secure API response headers", async ({ request }) => {
const response = await request.get("/api/users");
const headers = await getResponseHeaders(response);

expect(responseHeaderReport(headers, { type: "api" })).toMatchTextFile({
fileExtension: "md",
});
});
```

`getResponseHeaders` extracts all headers from a page response or an `APIRequestContext` response.

The report lists all evaluated headers of the response, followed by the recommendations:

```md
# Response Header Report

## Detected Headers

| Header | Value |
| ------------------------------ | -------------------------------- |
| `cache-control` | `no-store` |
| `content-security-policy` | `default-src 'self'` |
| `content-type` | `text/html` |
| `cross-origin-opener-policy` | `unsafe-none` |
| `cross-origin-resource-policy` | `same-origin` |
| `strict-transport-security` | `max-age=3600` |
| `x-content-type-options` | `nosniff` |
| `x-robots-tag` | `noindex` |
| `permissions-policy` | `camera=()` |
| `referrer-policy` | `unsafe-url` |
| `x-frame-options` | `ALLOW-FROM https://example.com` |

## Recommendations

- `cross-origin-opener-policy`: Change value to `same-origin`
- `strict-transport-security`: Change value to `max-age=31536000; includeSubDomains`
- `permissions-policy`: Change value to `geolocation=(), microphone=(), camera=()`
- `referrer-policy`: Change value to `no-referrer`
- `x-frame-options`: Change value to `DENY`
```

Headers which are neither recommended nor discouraged, like `date` or `etag`, are omitted to keep the report deterministic. Nonces in the `Content-Security-Policy` header are masked as `[NONCE_0]`, `[NONCE_1]`, … like in the [CSP Report](./csp-report.md).

Recommendations are grouped into missing headers to add, deviating headers to change and discouraged headers to remove. Header values are compared to the recommended value ignoring case and whitespace.

## Types

The `type` option selects the recommended headers. Use `document` for HTML documents rendered by the browser, and `api` for all other responses like REST or GraphQL endpoints.
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export {
} from "./api/route-interceptor/response-handler";

export { cspReport, getCspHeader } from "./security/csp-report";
export {
getResponseHeaders,
responseHeaderReport,
type ResponseHeaderReportOptions,
type ResponseHeaderType,
} from "./security/response-header-report";

export { resolveFromPackageRoot } from "./file";

Expand Down
Loading
Loading