Skip to content

Commit

Permalink
Fix CSP override issue and integrate with SvelteKit's nonce (#7)
Browse files Browse the repository at this point in the history
This commit addresses issue #7, where the library was overriding SvelteKit's
CSP directives without using the nonce value. The changes include:

- Modified securityHeaders.ts to integrate with SvelteKit's CSP
- Updated types and utilities to support nonce integration
- Adjusted demo app configuration to demonstrate proper CSP usage
- Renamed and reorganized files for better structure
  • Loading branch information
IslamZaoui committed Sep 29, 2024
1 parent 81bb6ac commit 7e4bad5
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 85 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ you can also use other package managers like [pnpm](https://pnpm.io/) or [yarn](

### default headers

this will add the default headers to the response, this will be enough to get your website A grade in [security headers](https://securityheaders.com/)

```ts
// src/hooks.server.ts
import { securityHeaders } from '@islamzaoui/securekit';
Expand All @@ -28,6 +30,8 @@ export const handle = securityHeaders().handle;

### with custom headers

you can customize the headers you want to set, and override any of the headers available in response headers.

```ts
// src/hooks.server.ts
import { securityHeaders } from '@islamzaoui/securekit';
Expand All @@ -46,6 +50,8 @@ export const handle = securityHeaders({

### with multiple handles

you can use [sequence](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) to sequencing other handles with `securityHeaders`.

```ts
// src/hooks.server.ts
import { securityHeaders } from '@islamzaoui/securekit';
Expand All @@ -61,10 +67,32 @@ export const handle = sequence(
);
```

### delete headers

set the value to `null` to delete the header.

```ts
// src/hooks.server.ts
import { securityHeaders } from '@islamzaoui/securekit';

export const handle = securityHeaders({
headers: {
'Access-Control-Allow-Origin': 'https://yoursite.com',
'x-sveltekit-page': null, // this will be deleted
},
});
```

## Content Security Policy header

your can use `csp` option in `securityHeaders` to set the `Content-Security-Policy` header directives.

**Note**:

- `config.csp` directives will override any existing directives in the `Content-Security-Policy` header.

- `config.csp` directives will extend the existing directives in [svelte.config.js](https://kit.svelte.dev/docs/configuration#csp)

```ts
// src/hooks.server.ts
import { securityHeaders } from '@islamzaoui/securekit';
Expand Down
9 changes: 5 additions & 4 deletions apps/demo/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { dev } from '$app/environment';
import { rules, securityHeaders } from '@islamzaoui/securekit';
import { securityHeaders, rules } from '@islamzaoui/securekit';

const origin = dev ? 'http://localhost:5173' : 'https://securekit-demo.vercel.app';

export const handle = securityHeaders({
headers: {
...rules.defaultHeaders,
'Access-Control-Allow-Origin': origin
'Access-Control-Allow-Origin': origin,
'x-sveltekit-page': null
},
csp: {
directives: {
Expand All @@ -21,9 +22,9 @@ export const handle = securityHeaders({
'manifest-src': ["'self'"],
'media-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'style-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'"],
'default-src': ["'self'", origin],
'script-src': ["'self'", "'unsafe-inline'"],
'script-src': ["'self'"],
'worker-src': ["'self'"]
}
},
Expand Down
8 changes: 7 additions & 1 deletion apps/demo/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
adapter: adapter(),
csp: {
mode: 'auto',
directives: {
'default-src': ["'self'"]
}
}
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTTPResponseHeaders } from './types/http';
import { HTTPResponseHeaders } from '../types/http';

const defaultHeaders: HTTPResponseHeaders = {
'X-Frame-Options': 'DENY',
Expand All @@ -7,7 +7,7 @@ const defaultHeaders: HTTPResponseHeaders = {
'Permissions-Policy': 'geolocation=(), camera=(), microphone=()',
};

export const rules = {
export default {
/**
* Default headers for secure headers.
* @example
Expand Down
62 changes: 3 additions & 59 deletions packages/securekit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,3 @@
import { Handle } from '@sveltejs/kit';
import { Config } from './types';
import { normalizeCspValue, normalizeHeaderValue } from './utils';
import { rules } from './headers';

/**
* sets HTTP response security headers based on the provided configuration.
*
* @param {Config} [config] - Configuration object for secure headers.
* @param {Object} [config.headers] - Custom headers to set.
* @param {boolean} [config.debug] - Enables debug logging if true.
* @param {Object} [config.csp] - Content Security Policy directives.
* @returns {Object} An object containing the handle function to process requests.
*/
export const securityHeaders = (
config: Config = {
headers: {
...rules.defaultHeaders,
},
debug: false,
csp: undefined,
},
) => {
const headers = new Map<string, string>();

if (config?.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
value && headers.set(key, normalizeHeaderValue(value));
});
}

if (config?.csp) {
const cspDirectives = Object.entries(config.csp.directives)
.map(([key, value]) => `${key} ${normalizeCspValue(value)}`)
.join('; ');
headers.set('Content-Security-Policy', cspDirectives);
}

const handle: Handle = async ({ event, resolve }) => {
if (config?.debug) {
console.log(
'[DEBUG] securekit Headers:',
JSON.stringify(Object.fromEntries(headers), null, 2),
);
}

const response = await resolve(event);

headers.forEach((value, key) => {
response.headers.set(key, value);
});

return response;
};

return { handle };
};

export { rules } from './headers';
export { securityHeaders } from './securityHeaders';
export { default as rules } from './constants/rules';
export * from './types';
98 changes: 98 additions & 0 deletions packages/securekit/src/securityHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Handle } from '@sveltejs/kit';
import rules from './constants/rules';
import { Config } from './types';
import {
normalizeCspValue,
normalizeHeaderValue,
} from './utils/headerNormalizers';
import { combineCspHeaders } from './utils/cspHelpers';

/**
* Sets HTTP response security headers based on the provided configuration.
*
* @param {Config} [config] - Configuration object for secure headers.
* @param {HTTPResponseHeaders} [config.headers] - Custom headers to set. Defaults to predefined security headers.
* @param {boolean} [config.debug=false] - Enables debug logging if true.
* @param {Csp} [config.csp] - Content Security Policy directives.
*
* @returns {Object} An object containing the handle function to process requests.
* @property {Handle} handle - A SvelteKit handle function that applies the configured security headers to each response.
*
* @example
* // Basic usage with default settings
* export const handle = securityHeaders().handle;
*
* @example
* // Custom configuration
* export const handle = securityHeaders({
* headers: {
* 'Access-Control-Allow-Origin': 'https://example.com',
* 'X-Frame-Options': 'SAMEORIGIN'
* },
* debug: true,
* csp: {
* directives: {
* 'default-src': ["'self'"],
* 'script-src': ["'self'", 'https://trusted-cdn.com']
* }
* }
* }).handle;
*/
export const securityHeaders = (
config: Config = {
headers: {
...rules.defaultHeaders,
},
debug: false,
csp: undefined,
},
) => {
const headers = new Map<string, string | null>();

if (config?.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
if (value !== undefined)
headers.set(key, normalizeHeaderValue(value));
});
}

if (config?.csp) {
const cspDirectives = Object.entries(config.csp.directives)
.map(([key, value]) => `${key} ${normalizeCspValue(value)}`)
.join('; ');
headers.set('Content-Security-Policy', cspDirectives);
}

const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);

headers.forEach((value, key) => {
if (key.toLocaleLowerCase() === 'content-security-policy') {
const existingCsp = response.headers.get(
'Content-Security-Policy',
);
const combinedCsp = combineCspHeaders(existingCsp, value);
if (combinedCsp) {
response.headers.set(
'Content-Security-Policy',
combinedCsp,
);
}
} else {
if (value) response.headers.set(key, value);
else response.headers.delete(key);
}
});

if (config?.debug) {
console.log(
'[DEBUG] securekit Headers:\n',
JSON.stringify(Object.fromEntries(response.headers), null, 2),
);
}

return response;
};

return { handle };
};
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,14 @@ export interface CspDirectives {
}

export interface Csp {
/**
* Content Security Policy header directives.
* @example
* directives: {
* 'default-src': ['self'],
* 'script-src': ['self', 'https://example.com'],
* 'style-src': ['self', 'https://example.com'],
* }
*/
directives: CspDirectives;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,58 +36,58 @@ export type HTTPResponseHeaders = {
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security | Strict-Transport-Security MDN}
*/
'Strict-Transport-Security'?: string;
'Strict-Transport-Security'?: string | null;
/**
* Better to use csp in securekit function instead
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy | Content-Security-Policy MDN}
*/
'Content-Security-Policy'?: string;
'Content-Security-Policy'?: string | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy | Referrer-Policy MDN}
*/
'Referrer-Policy'?: ReferrerPolicy;
'Referrer-Policy'?: ReferrerPolicy | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy | Permissions-Policy MDN}
*/
'Permissions-Policy'?: string;
'Permissions-Policy'?: string | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection | X-XSS-Protection MDN}
*/
'X-XSS-Protection'?: string;
'X-XSS-Protection'?: string | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options | X-Frame-Options MDN}
*/
'X-Frame-Options'?: 'DENY' | 'SAMEORIGIN';
'X-Frame-Options'?: 'DENY' | 'SAMEORIGIN' | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options | X-Content-Type-Options MDN}
*/
'X-Content-Type-Options'?: 'nosniff';
'X-Content-Type-Options'?: 'nosniff' | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin | Access-Control-Allow-Origin MDN}
*/
'Access-Control-Allow-Origin'?: '*' | string | string[];
'Access-Control-Allow-Origin'?: '*' | string | string[] | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods | Access-Control-Allow-Methods MDN}
*/
'Access-Control-Allow-Methods'?: '*' | HTTPMethods | HTTPMethods[];
'Access-Control-Allow-Methods'?: '*' | HTTPMethods | HTTPMethods[] | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers | Access-Control-Allow-Headers MDN}
*/
'Access-Control-Allow-Headers'?: '*' | string | string[];
'Access-Control-Allow-Headers'?: '*' | string | string[] | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age | Access-Control-Max-Age MDN}
*/
'Access-Control-Max-Age'?: number;
'Access-Control-Max-Age'?: number | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy | Cross-Origin-Embedder-Policy MDN}
*/
'Cross-Origin-Embedder-Policy'?: CrossOriginEmbedderPolicy;
'Cross-Origin-Embedder-Policy'?: CrossOriginEmbedderPolicy | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy | Cross-Origin-Opener-Policy MDN}
*/
'Cross-Origin-Opener-Policy'?: CrossOriginOpenerPolicy;
'Cross-Origin-Opener-Policy'?: CrossOriginOpenerPolicy | null;
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy | Cross-Origin-Resource-Policy MDN}
*/
'Cross-Origin-Resource-Policy'?: CrossOriginResourcePolicy;
'Cross-Origin-Resource-Policy'?: CrossOriginResourcePolicy | null;
};
2 changes: 1 addition & 1 deletion packages/securekit/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type Config = {
/**
* Content Security Policy Directives to be applied.
*
* This will override any CSP headers set in `headers`.
* This will override any CSP headers set in `config.headers`, and extend csp directives in your `svelte.config.js`.
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
* @default undefined
*/
Expand Down
Loading

0 comments on commit 7e4bad5

Please sign in to comment.