Skip to content

Commit

Permalink
move next-safe into code (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Enalmada authored Oct 6, 2023
1 parent 09e0a9e commit d211a5f
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-bananas-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@enalmada/next-secure': minor
---

move next-safe into library
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"overrides": [
{
"extends": ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
"files": ["*.ts", "*.tsx"],
"files": ["src/**/*.ts", "src/**/*.tsx"],
"parserOptions": {
"project": "./tsconfig.json",
"sourceType": "module"
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Run Tests

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
inputs:
target_url:
description: 'Deployment target URL'
required: true

jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install
- run: bun run test:unit
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ bun add @enalmada/next-secure

## Usage


### In middleware.ts



### In next.config.cjs

```ts
// next.config.mjs
// @ts-check
Expand Down
12 changes: 6 additions & 6 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
target: 'node',
external: ['*'],
root: './src',
});
entrypoints: ['./src/index.ts'],
outdir: './dist',
target: 'node',
external: ['next-safe'],
root: './src',
});
Binary file modified bun.lockb
Binary file not shown.
42 changes: 12 additions & 30 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,4 @@
export declare const Default: {
contentTypeOptions: string;
contentSecurityPolicy: {
'base-uri': string;
'child-src': string;
'connect-src': string;
'default-src': string;
'font-src': string;
'form-action': string;
'frame-ancestors': string;
'frame-src': string;
'img-src': string;
'manifest-src': string;
'media-src': string;
'object-src': string;
'prefetch-src': string;
'script-src': string;
'style-src': string;
'worker-src': string;
mergeDefaultDirectives: boolean;
reportOnly: boolean;
};
frameOptions: string;
permissionsPolicyDirectiveSupport: string[];
isDev: boolean;
referrerPolicy: string;
xssProtection: string;
};
export declare const Default: NextSafeConfig;
export interface CspRule {
description?: string;
source?: string;
Expand All @@ -45,6 +18,8 @@ export interface CspRule {
'default-src'?: string | boolean;
'form-action'?: string | boolean;
'frame-ancestors'?: string | boolean;
'block-all-mixed-content'?: boolean;
'upgrade-insecure-requests'?: boolean;
}
export interface ContentSecurityPolicyTemplate {
source?: string;
Expand All @@ -59,5 +34,12 @@ export interface ContentSecurityPolicyTemplate {
permissionsPolicyDirectiveSupport: any[];
isDev: boolean;
}
declare function generateCspTemplate(cspConfig: ContentSecurityPolicyTemplate, cspRules: CspRule[]): ContentSecurityPolicyTemplate[];
export { generateCspTemplate };
export type SourceHeaders = {
source: string;
headers: string;
};
declare function generateCspTemplates(cspConfig: ContentSecurityPolicyTemplate, cspRules: CspRule[], keysToRemove?: string[]): {
source: string;
headers: Header[];
}[];
export { generateCspTemplates };
15 changes: 14 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// src/index.ts
import * as nextSafe from "next-safe";
var deepMerge = function(target, source) {
const output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Expand Down Expand Up @@ -68,6 +69,15 @@ var generateCspTemplate = function(cspConfig, cspRules) {
}
return finalConfigs;
};
var generateCspTemplates = function(cspConfig, cspRules, keysToRemove = defaultKeysToRemove) {
const contentSecurityPolicyTemplates = generateCspTemplate(cspConfig, cspRules);
return contentSecurityPolicyTemplates.map((template) => {
return {
source: template.source || "/:path*",
headers: nextSafe.default({ ...template }).filter((header) => !keysToRemove.includes(header.key))
};
});
};
var Default = {
contentTypeOptions: "nosniff",
contentSecurityPolicy: {
Expand All @@ -87,6 +97,8 @@ var Default = {
"script-src": "'self'",
"style-src": "'self'",
"worker-src": "'self'",
"block-all-mixed-content": true,
"upgrade-insecure-requests": true,
mergeDefaultDirectives: false,
reportOnly: false
},
Expand All @@ -96,7 +108,8 @@ var Default = {
referrerPolicy: "no-referrer",
xssProtection: "1; mode=block"
};
var defaultKeysToRemove = ["Feature-Policy", "X-Content-Security-Policy", "X-WebKit-CSP"];
export {
generateCspTemplate,
generateCspTemplates,
Default
};
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
"scripts": {
"build": "rm -rf dist && bun build.mjs && bun run build:declaration",
"build:declaration": "tsc --emitDeclarationOnly",
"lint": "eslint --ext .ts,.tsx,.cjs,.mjs && bun type-check",
"lint:fix": "eslint --fix --ext .ts,.tsx,.cjs,.mjs && bun type-check",
"lint": "eslint . --ext .ts,.tsx,.cjs,.mjs && bun type-check",
"lint:fix": "eslint . --fix --ext .ts,.tsx,.cjs,.mjs && bun type-check",
"precommit": "bun lint-staged",
"prepare": "husky install",
"release": "bun run build && bunx changeset publish",
"type-check": "bun --bun tsc --noEmit"
"type-check": "bun --bun tsc --noEmit",
"test": "bun run test:unit",
"test:unit": "vitest"
},
"dependencies": {
"next-safe": "^3.4.1"
},
"dependencies": {},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",
"@types/node": "^20.6.5",
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.50.0",
Expand All @@ -24,7 +28,8 @@
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
43 changes: 41 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return */
import * as nextSafe from 'next-safe';

function deepMerge<T extends object, S extends object>(target: T, source: S): T & S {
const output: any = Object.assign({}, target); // using any here to bypass type restrictions, handle with care

Expand Down Expand Up @@ -27,7 +30,8 @@ function isObject(item: any): boolean {
}

// default configuration https://trezy.gitbook.io/next-safe/usage/configuration
export const Default = {

export const Default: NextSafeConfig = {
contentTypeOptions: 'nosniff',
contentSecurityPolicy: {
'base-uri': "'none'",
Expand All @@ -46,6 +50,10 @@ export const Default = {
'script-src': "'self'",
'style-src': "'self'",
'worker-src': "'self'",
// @ts-ignore
'block-all-mixed-content': true,
// @ts-ignore
'upgrade-insecure-requests': true,
mergeDefaultDirectives: false,
reportOnly: false,
},
Expand Down Expand Up @@ -75,6 +83,8 @@ export interface CspRule {
'default-src'?: string | boolean;
'form-action'?: string | boolean;
'frame-ancestors'?: string | boolean;
'block-all-mixed-content'?: boolean;
'upgrade-insecure-requests'?: boolean;
}

export interface ContentSecurityPolicyTemplate {
Expand All @@ -90,6 +100,7 @@ export interface ContentSecurityPolicyTemplate {
permissionsPolicyDirectiveSupport: any[];
isDev: boolean;
}

function groupBySource(cspRules: CspRule[]): Record<string, CspRule[]> {
const grouped: Record<string, CspRule[]> = {};
cspRules.forEach((rule) => {
Expand Down Expand Up @@ -149,4 +160,32 @@ function generateCspTemplate(
return finalConfigs;
}

export { generateCspTemplate };
export type SourceHeaders = {
source: string;
headers: string;
};

// next-safe adds legacy keys that are unnecessary and cause console noise
const defaultKeysToRemove = ['Feature-Policy', 'X-Content-Security-Policy', 'X-WebKit-CSP'];

function generateCspTemplates(
cspConfig: ContentSecurityPolicyTemplate,
cspRules: CspRule[],
keysToRemove: string[] = defaultKeysToRemove
) {
const contentSecurityPolicyTemplates = generateCspTemplate(cspConfig, cspRules);

return contentSecurityPolicyTemplates.map((template: ContentSecurityPolicyTemplate) => {
return {
source: template.source || '/:path*',
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
headers: nextSafe
// @ts-ignore
.default({ ...template })
.filter((header: { key: string }) => !keysToRemove.includes(header.key)) as Header[],
};
});
}

export { generateCspTemplates };
113 changes: 113 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { generateCspTemplates, type ContentSecurityPolicyTemplate, type CspRule } from '../src';

function getCspConfig(isDev: boolean): ContentSecurityPolicyTemplate {
return {
isDev: isDev,
contentSecurityPolicy: {
mergeDefaultDirectives: true,
'prefetch-src': false,
},
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: {
'ambient-light-sensor': false,
battery: false,
'document-domain': false,
'execution-while-not-rendered': false,
'execution-while-out-of-viewport': false,
'navigation-override': false,
'speaker-selection': false,
},
permissionsPolicyDirectiveSupport: ['proposed', 'standard'],
};
}

// https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#cross_origin_opener_policy
export const cspRules: CspRule[] = [
{
description: 'nextjs',
'script-src': 'http://testscript',
source: '/:path*',
},
];

describe('CSP Template Generation', () => {
it('basic dev test', () => {
const templates = generateCspTemplates(getCspConfig(true), cspRules);

const expectedTemplates = [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value:
"base-uri 'none';child-src 'none';connect-src 'self' webpack://*;default-src 'self';font-src 'self';form-action 'self';frame-ancestors 'none';frame-src 'none';img-src 'self';manifest-src 'self';media-src 'self';object-src 'none';script-src 'self' http://testscript 'unsafe-eval';style-src 'self' 'unsafe-inline';worker-src 'self';block-all-mixed-content ;upgrade-insecure-requests ;",
},
{
key: 'Permissions-Policy',
value:
'clipboard-read=(),clipboard-write=(),gamepad=(),accelerometer=(),autoplay=(),camera=(),cross-origin-isolated=(),display-capture=(),encrypted-media=(),fullscreen=(),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(),usb=(),web-share=(),xr-spatial-tracking=()',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
];

expect(templates).toEqual(expectedTemplates);
});

it('basic prod test', () => {
const templates = generateCspTemplates(getCspConfig(false), cspRules);

const expectedTemplates = [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value:
"base-uri 'none';child-src 'none';connect-src 'self';default-src 'self';font-src 'self';form-action 'self';frame-ancestors 'none';frame-src 'none';img-src 'self';manifest-src 'self';media-src 'self';object-src 'none';script-src 'self' http://testscript;style-src 'self';worker-src 'self';block-all-mixed-content ;upgrade-insecure-requests ;",
},
{
key: 'Permissions-Policy',
value:
'clipboard-read=(),clipboard-write=(),gamepad=(),accelerometer=(),autoplay=(),camera=(),cross-origin-isolated=(),display-capture=(),encrypted-media=(),fullscreen=(),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(),usb=(),web-share=(),xr-spatial-tracking=()',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
];

expect(templates).toEqual(expectedTemplates);
});
});
Loading

0 comments on commit d211a5f

Please sign in to comment.