Skip to content
Merged
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
75 changes: 75 additions & 0 deletions src/middleware/secure-headers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,81 @@ describe('Secure Headers Middleware', () => {
})
})

describe('CSP with combined modes', () => {
it('keeps the enforced policy when report-only uses a nonce', async () => {
const app = new Hono()
app.use(
'/test',
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
},
contentSecurityPolicyReportOnly: {
scriptSrc: ["'self'", NONCE],
},
})
)
app.all('*', (c) => c.text('test'))

const res = await app.request('/test')

expect(res.status).toBe(200)
expect(res.headers.get('Content-Security-Policy')).toBe("default-src 'self'")
expect(res.headers.get('Content-Security-Policy-Report-Only')).toMatch(
/^script-src 'self' 'nonce-[a-zA-Z0-9+/]+=*'$/
)
})

it('keeps the report-only policy when the enforced policy uses a nonce', async () => {
const app = new Hono()
app.use(
'/test',
secureHeaders({
contentSecurityPolicy: {
scriptSrc: ["'self'", NONCE],
},
contentSecurityPolicyReportOnly: {
defaultSrc: ["'self'"],
},
})
)
app.all('*', (c) => c.text('test'))

const res = await app.request('/test')

expect(res.status).toBe(200)
expect(res.headers.get('Content-Security-Policy')).toMatch(
/^script-src 'self' 'nonce-[a-zA-Z0-9+/]+=*'$/
)
expect(res.headers.get('Content-Security-Policy-Report-Only')).toBe("default-src 'self'")
})

it('supports nonces in both policies', async () => {
const app = new Hono()
app.use(
'/test',
secureHeaders({
contentSecurityPolicy: {
scriptSrc: ["'self'", NONCE],
},
contentSecurityPolicyReportOnly: {
styleSrc: ["'self'", NONCE],
},
})
)
app.all('*', (c) => c.text('test'))

const res = await app.request('/test')
const csp = res.headers.get('Content-Security-Policy')
const reportOnly = res.headers.get('Content-Security-Policy-Report-Only')
const nonce = csp?.match(/'nonce-([^']+)'/)?.[1]

expect(res.status).toBe(200)
expect(nonce).toBeTruthy()
expect(reportOnly).toContain(`'nonce-${nonce}'`)
})
})

// OUR NEW REPORT-URI TESTS
describe('CSP report-uri directive', () => {
it('should set report-uri with single endpoint', async () => {
Expand Down
18 changes: 11 additions & 7 deletions src/middleware/secure-headers/secure-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,21 @@ export const secureHeaders = (customOptions?: SecureHeadersOptions): MiddlewareH
const callbacks: SecureHeadersCallback[] = []

if (options.contentSecurityPolicy) {
const [callback, value] = getCSPDirectives(options.contentSecurityPolicy)
const [callback, value] = getCSPDirectives(
options.contentSecurityPolicy,
'Content-Security-Policy'
)
if (callback) {
callbacks.push(callback)
}
headersToSet.push(['Content-Security-Policy', value as string])
}

if (options.contentSecurityPolicyReportOnly) {
const [callback, value] = getCSPDirectives(options.contentSecurityPolicyReportOnly)
const [callback, value] = getCSPDirectives(
options.contentSecurityPolicyReportOnly,
'Content-Security-Policy-Report-Only'
)
if (callback) {
callbacks.push(callback)
}
Expand Down Expand Up @@ -238,7 +244,8 @@ function getFilteredHeaders(options: SecureHeadersOptions): [string, string][] {
}

function getCSPDirectives(
contentSecurityPolicy: ContentSecurityPolicyOptions
contentSecurityPolicy: ContentSecurityPolicyOptions,
headerName: 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only'
): [SecureHeadersCallback | undefined, string | string[]] {
const callbacks: ((ctx: Context, values: string[]) => void)[] = []
const resultValues: string[] = []
Expand Down Expand Up @@ -270,10 +277,7 @@ function getCSPDirectives(
: [
(ctx, headersToSet) =>
headersToSet.map((values) => {
if (
values[0] === 'Content-Security-Policy' ||
values[0] === 'Content-Security-Policy-Report-Only'
) {
if (values[0] === headerName) {
const clone = values[1].slice() as unknown as string[]
callbacks.forEach((cb) => {
cb(ctx, clone)
Expand Down
Loading