Skip to content

Commit

Permalink
feat: add CSP support (#29)
Browse files Browse the repository at this point in the history
Closes #9
  • Loading branch information
ValeraS authored Oct 16, 2023
1 parent fe0014d commit 85e2a92
Show file tree
Hide file tree
Showing 19 changed files with 357 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,32 @@ app.run();
```

More complex examples and documentation are coming.

## CSP

`config.ts`

```typescript
import type {AppConfig} from '@gravity-ui/nodekit';
import {csp} from '@gravity-ui/expresskit';

const config: Partial<AppConfig> = {
expressCspEnable: true,
expressCspPresets: ({getDefaultPresets}) => {
return getDefaultPresets({defaultNone: true}).concat([
csp.inline(),
{csp.directives.REPORT_TO: 'my-report-group'},
]);
},
expressCspReportTo: [
{
group: 'my-report-group',
max_age: 30 * 60,
endpoints: [{ url: 'https://cspreport.com/send'}],
include_subdomains: true,
}
]
}

export default config;
```
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"dependencies": {
"body-parser": "^1.20.1",
"cookie-parser": "1.4.6",
"csp-header": "^5.2.1",
"express": "^4.18.2",
"express-csp-header": "^5.2.1",
"uuid": "^9.0.0"
},
"peerDependencies": {
Expand Down
57 changes: 57 additions & 0 deletions src/csp/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
BLOB,
DATA,
NONCE,
NONE,
SELF,
STRICT_DYNAMIC,
TLD,
EVAL as UNSAFE_EVAL,
INLINE as UNSAFE_INLINE,
WASM_UNSAFE_EVAL,
} from 'express-csp-header';
import type {CSPDirectiveName} from 'express-csp-header';

export const CSPValues = {
NONE,
SELF,
NONCE,
UNSAFE_EVAL,
WASM_UNSAFE_EVAL,
UNSAFE_INLINE,
TLD,
STRICT_DYNAMIC,
DATA,
BLOB,
} as const;

export const CSPDirectives = {
BASE_URI: 'base-uri',
MIXED_CONTENT: 'block-all-mixed-content',
CHILD: 'child-src',
CONNECT: 'connect-src',
DEFAULT: 'default-src',
FONT: 'font-src',
FORM: 'form-action',
FRAME_ANCESTORS: 'frame-ancestors',
FRAME: 'frame-src',
IMG: 'img-src',
MANIFEST: 'manifest-src',
MEDIA: 'media-src',
OBJECT: 'object-src',
PLUGIN_TYPES: 'plugin-types',
PREFETCH: 'prefetch-src',
REFERRER: 'referrer',
REPORT: 'report-uri',
REPORT_TO: 'report-to',
SANDBOX: 'sandbox',
SCRIPT: 'script-src',
SCRIPT_ATTR: 'script-src-attr',
SCRIPT_ELEM: 'script-src-elem',
STYLE: 'style-src',
STYLE_ATTR: 'style-src-attr',
STYLE_ELEM: 'style-src-elem',
INSECURE: 'upgrade-insecure-requests',
WEBRTC: 'webrtc',
WORKER: 'worker-src',
} satisfies Record<string, CSPDirectiveName>;
15 changes: 15 additions & 0 deletions src/csp/default-presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {CSPPresetsArray} from 'csp-header';

import {csp} from '.';

export function getDefaultPresets({defaultNone}: {defaultNone?: boolean} = {}) {
const presets: CSPPresetsArray = [csp.self(), csp.nonce(), csp.data()];

if (defaultNone) {
presets.unshift(csp.none([csp.directives.DEFAULT]));
} else {
presets.unshift(csp.self([csp.directives.DEFAULT]));
}

return presets;
}
28 changes: 28 additions & 0 deletions src/csp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {CSPPresetsArray} from 'csp-header';

import {CSPDirectives, CSPValues} from './constants';
import {dataRules} from './services/data';
import {dynamicRules} from './services/dynamic';
import {evalRules} from './services/eval';
import {inlineRules} from './services/inline';
import {nonceRules} from './services/nonce';
import {noneRules} from './services/none';
import {selfRules} from './services/self';
import {makeCspObject} from './utils';

export type CSPPreset = CSPPresetsArray;

export const csp = {
directives: CSPDirectives,
values: CSPValues,

none: noneRules,
self: selfRules,
inline: inlineRules,
eval: evalRules,
nonce: nonceRules,
data: dataRules,
dynamic: dynamicRules,

makeCspObject,
};
41 changes: 41 additions & 0 deletions src/csp/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {expressCspHeader} from 'express-csp-header';
import type {ExpressCSPParams} from 'express-csp-header';

import {getDefaultPresets} from './default-presets';

import type {CSPPreset} from '.';

export interface CSPMiddlewareParams extends Omit<ExpressCSPParams, 'presets'> {
appPresets: CSPPreset;
routPresets?:
| CSPPreset
| ((params: {
getDefaultPresets: typeof getDefaultPresets;
appPresets: CSPPreset;
}) => CSPPreset);
}

export function cspMiddleware(options: CSPMiddlewareParams) {
let presets: CSPPreset = options.appPresets;
if (options.routPresets) {
presets =
typeof options.routPresets === 'function'
? options.routPresets({getDefaultPresets, appPresets: presets})
: options.routPresets;
}

return expressCspHeader({...options, presets});
}

export function getAppPresets(
presets?: CSPPreset | ((params: {getDefaultPresets: typeof getDefaultPresets}) => CSPPreset),
) {
let appPresets: CSPPreset;
if (presets) {
appPresets = typeof presets === 'function' ? presets({getDefaultPresets}) : presets;
} else {
appPresets = getDefaultPresets();
}

return appPresets;
}
9 changes: 9 additions & 0 deletions src/csp/services/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function dataRules(
sources: DirectivesOfType<(typeof CSPValues.DATA)[]>[] = [CSPDirectives.IMG, 'trusted-types'],
) {
return makeCspObject(sources, [CSPValues.DATA]);
}
9 changes: 9 additions & 0 deletions src/csp/services/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function dynamicRules(
sources: DirectivesOfType<(typeof CSPValues.STRICT_DYNAMIC)[]>[] = [CSPDirectives.SCRIPT],
) {
return makeCspObject(sources, [CSPValues.STRICT_DYNAMIC]);
}
9 changes: 9 additions & 0 deletions src/csp/services/eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function evalRules(
sources: DirectivesOfType<(typeof CSPValues.UNSAFE_EVAL)[]>[] = [CSPDirectives.SCRIPT],
) {
return makeCspObject(sources, [CSPValues.UNSAFE_EVAL]);
}
12 changes: 12 additions & 0 deletions src/csp/services/inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function inlineRules(
sources: DirectivesOfType<(typeof CSPValues.UNSAFE_INLINE)[]>[] = [
CSPDirectives.SCRIPT,
CSPDirectives.STYLE,
],
) {
return makeCspObject(sources, [CSPValues.UNSAFE_INLINE]);
}
9 changes: 9 additions & 0 deletions src/csp/services/nonce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function nonceRules(
sources: DirectivesOfType<(typeof CSPValues.NONCE)[]>[] = [CSPDirectives.SCRIPT],
) {
return makeCspObject(sources, [CSPValues.NONCE]);
}
9 changes: 9 additions & 0 deletions src/csp/services/none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function noneRules(
sources: DirectivesOfType<(typeof CSPValues.NONE)[]>[] = [CSPDirectives.DEFAULT],
) {
return makeCspObject(sources, [CSPValues.NONE]);
}
18 changes: 18 additions & 0 deletions src/csp/services/self.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {CSPDirectives, CSPValues} from '../constants';
import type {DirectivesOfType} from '../types';
import {makeCspObject} from '../utils';

export function selfRules(
sources: DirectivesOfType<(typeof CSPValues.SELF)[]>[] = [
CSPDirectives.SCRIPT,
CSPDirectives.STYLE,
CSPDirectives.FONT,
CSPDirectives.IMG,
CSPDirectives.MEDIA,
CSPDirectives.FRAME,
CSPDirectives.CHILD,
CSPDirectives.CONNECT,
],
) {
return makeCspObject(sources, [CSPValues.SELF]);
}
16 changes: 16 additions & 0 deletions src/csp/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {CSPDirectives} from 'csp-header';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;

export type DirectivesOfType<T> = {
[K in keyof CSPDirectives]: T extends CSPDirectives[K] ? K : never;
}[keyof CSPDirectives];

export type CommonTypeOfDirectives<T> = UnionToIntersection<
T extends keyof CSPDirectives ? {Type: CSPDirectives[T]} : never
> extends Record<string, infer P>
? P
: never;
13 changes: 13 additions & 0 deletions src/csp/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {CSPDirectiveName} from 'csp-header';

import type {CommonTypeOfDirectives} from './types';

export function makeCspObject<T extends CSPDirectiveName>(
sources: T[],
data: CommonTypeOfDirectives<T>,
) {
return sources.reduce((acc, key) => {
acc[key] = data;
return acc;
}, {} as Record<T, CommonTypeOfDirectives<T>>);
}
Loading

0 comments on commit 85e2a92

Please sign in to comment.