diff --git a/__snapshots__/config.default.test.ts.js b/__snapshots__/config.default.test.ts.js new file mode 100644 index 0000000..d703212 --- /dev/null +++ b/__snapshots__/config.default.test.ts.js @@ -0,0 +1,84 @@ +exports['test/config/config.default.test.ts should config default values keep stable 1'] = { + "security": { + "domainWhiteList": [], + "protocolWhiteList": [], + "defaultMiddleware": [ + "csrf", + "hsts", + "methodnoallow", + "noopen", + "nosniff", + "csp", + "xssProtection", + "xframe", + "dta" + ], + "csrf": { + "enable": true, + "type": "ctoken", + "ignoreJSON": false, + "cookieName": "csrfToken", + "sessionName": "csrfToken", + "headerName": "x-csrf-token", + "bodyName": "_csrf", + "queryName": "_csrf", + "rotateWhenInvalid": false, + "useSession": false, + "supportedRequests": [ + { + "path": {}, + "methods": [ + "POST", + "PATCH", + "DELETE", + "PUT", + "CONNECT" + ] + } + ], + "refererWhiteList": [], + "cookieOptions": { + "signed": false, + "httpOnly": false, + "overwrite": true + } + }, + "xframe": { + "enable": true, + "value": "SAMEORIGIN" + }, + "hsts": { + "enable": false, + "maxAge": 31536000, + "includeSubdomains": false + }, + "methodnoallow": { + "enable": true + }, + "noopen": { + "enable": true + }, + "nosniff": { + "enable": true + }, + "xssProtection": { + "enable": true, + "value": "1; mode=block" + }, + "csp": { + "enable": false, + "policy": {} + }, + "referrerPolicy": { + "enable": false, + "value": "no-referrer-when-downgrade" + }, + "dta": { + "enable": true + }, + "ssrf": {} + }, + "helper": { + "shtml": {} + } +} diff --git a/__snapshots__/csp.test.ts.js b/__snapshots__/csp.test.ts.js new file mode 100644 index 0000000..cadb903 --- /dev/null +++ b/__snapshots__/csp.test.ts.js @@ -0,0 +1,90 @@ +exports['test/csp.test.ts should ignore path 1'] = { + "domainWhiteList": [], + "protocolWhiteList": [], + "defaultMiddleware": "csp", + "csrf": { + "enable": true, + "type": "ctoken", + "ignoreJSON": false, + "cookieName": "csrfToken", + "sessionName": "csrfToken", + "headerName": "x-csrf-token", + "bodyName": "_csrf", + "queryName": "_csrf", + "rotateWhenInvalid": false, + "useSession": false, + "supportedRequests": [ + { + "path": {}, + "methods": [ + "POST", + "PATCH", + "DELETE", + "PUT", + "CONNECT" + ] + } + ], + "refererWhiteList": [], + "cookieOptions": { + "signed": false, + "httpOnly": false, + "overwrite": true + } + }, + "xframe": { + "enable": true, + "value": "SAMEORIGIN" + }, + "hsts": { + "enable": false, + "maxAge": 31536000, + "includeSubdomains": false + }, + "methodnoallow": { + "enable": true + }, + "noopen": { + "enable": true + }, + "nosniff": { + "enable": true + }, + "xssProtection": { + "enable": true, + "value": "1; mode=block" + }, + "csp": { + "ignore": "/api/", + "enable": true, + "policy": { + "script-src": [ + "'self'", + "'unsafe-inline'", + "'unsafe-eval'", + "www.google-analytics.com" + ], + "style-src": [ + "'unsafe-inline'", + "www.google-analytics.com" + ], + "img-src": [ + "'self'", + "data:", + "www.google-analytics.com" + ], + "frame-ancestors": [ + "'self'" + ], + "report-uri": "http://pointman.domain.com/csp?app=csp" + } + }, + "referrerPolicy": { + "enable": false, + "value": "no-referrer-when-downgrade" + }, + "dta": { + "enable": true + }, + "ssrf": {} +} diff --git a/package.json b/package.json index c834350..d0bb70c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "matcher": "^4.0.0", "nanoid": "^3.3.8", "type-is": "^1.6.18", - "xss": "^1.0.3" + "xss": "^1.0.3", + "zod": "^3.24.1" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.1", @@ -66,6 +67,7 @@ "eslint": "8", "eslint-config-egg": "14", "rimraf": "6", + "snap-shot-it": "^7.9.10", "spy": "^1.0.0", "supertest": "^6.3.3", "tshy": "3", diff --git a/src/app.ts b/src/app.ts index 92d97f4..6e670fd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import type { ILifecycleBoot, EggCore } from '@eggjs/core'; import { preprocessConfig } from './lib/utils.js'; +import { SecurityConfig } from './config/config.default.js'; export default class AgentBoot implements ILifecycleBoot { private readonly app; @@ -24,6 +25,8 @@ export default class AgentBoot implements ILifecycleBoot { '[@eggjs/security/ap] `config.security.csrf.type` must be one of ' + legalTypes.join(', ')); } + // parse config and check if config is legal + app.config.security = SecurityConfig.parse(app.config.security); preprocessConfig(app.config.security); } } diff --git a/src/app/middleware/securities.ts b/src/app/middleware/securities.ts index 94e6e67..ab67437 100644 --- a/src/app/middleware/securities.ts +++ b/src/app/middleware/securities.ts @@ -3,11 +3,14 @@ import compose from 'koa-compose'; import { pathMatching } from 'egg-path-matching'; import { EggCore, MiddlewareFunc } from '@eggjs/core'; import securityMiddlewares from '../../lib/middlewares/index.js'; +import type { SecurityMiddlewareName } from '../../config/config.default.js'; export default (_: unknown, app: EggCore) => { const options = app.config.security; const middlewares: MiddlewareFunc[] = []; - const defaultMiddleware = options.defaultMiddleware.split(',').map(m => m.trim()).filter(m => !!m); + const defaultMiddlewares = typeof options.defaultMiddleware === 'string' + ? options.defaultMiddleware.split(',').map(m => m.trim()).filter(m => !!m) as SecurityMiddlewareName[] + : options.defaultMiddleware; if (options.match || options.ignore) { app.coreLogger.warn('[@eggjs/security/middleware/securities] Please set `match` or `ignore` on sub config'); @@ -19,8 +22,8 @@ export default (_: unknown, app: EggCore) => { options.csrf.cookieDomain = () => originalCookieDomain; } - defaultMiddleware.forEach(middlewareName => { - const opt = Reflect.get(options, middlewareName); + defaultMiddlewares.forEach(middlewareName => { + const opt = Reflect.get(options, middlewareName) as any; if (opt === false) { app.coreLogger.warn('[egg-security] Please use `config.security.%s = { enable: false }` instead of `config.security.%s = false`', middlewareName, middlewareName); } @@ -49,10 +52,7 @@ export default (_: unknown, app: EggCore) => { opt.matching = pathMatching(opt); const createMiddleware = securityMiddlewares[middlewareName]; - if (!createMiddleware) { - throw new TypeError(`[@eggjs/security/middleware/securities] Can't find middleware ${middlewareName}`); - } - const fn = createMiddleware(opt, app); + const fn = createMiddleware(opt); middlewares.push(fn); app.coreLogger.info('[@eggjs/security/middleware/securities] use %s middleware', middlewareName); }); diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 47e604a..ff7affb 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -1,96 +1,372 @@ -import { SecurityConfig, SecurityHelperConfig } from '../types.js'; +import z from 'zod'; -export default { - security: { - domainWhiteList: [], - protocolWhiteList: [], - defaultMiddleware: 'csrf,hsts,methodnoallow,noopen,nosniff,csp,xssProtection,xframe,dta', - - csrf: { - enable: true, - - // can be ctoken or referer or all - type: 'ctoken', - ignoreJSON: false, - - // These config works when using ctoken type - useSession: false, - // can be function(ctx) or String - cookieDomain: undefined, - cookieName: 'csrfToken', - sessionName: 'csrfToken', - headerName: 'x-csrf-token', - bodyName: '_csrf', - queryName: '_csrf', - rotateWhenInvalid: false, - supportedRequests: [ - { path: /^\//, methods: [ 'POST', 'PATCH', 'DELETE', 'PUT', 'CONNECT' ] }, - ], - - // These config works when using referer type - refererWhiteList: [ - // 'eggjs.org' - ], - // csrf token's cookie options - cookieOptions: { - signed: false, - httpOnly: false, - overwrite: true, - }, - }, +const CSRFSupportRequestItem = z.object({ + path: z.instanceof(RegExp), + methods: z.array(z.string()), +}); +export type CSRFSupportRequestItem = z.infer; - xframe: { - enable: true, - // 'SAMEORIGIN', 'DENY' or 'ALLOW-FROM http://example.jp' - value: 'SAMEORIGIN', - }, +export const LookupAddress = z.object({ + address: z.string(), + family: z.number(), +}); +export type LookupAddress = z.infer; - hsts: { - enable: false, - maxAge: 365 * 24 * 3600, - includeSubdomains: false, - }, +const LookupAddressAndStringArray = z.union([ z.string(), LookupAddress ]).array(); +const SSRFCheckAddressFunction = z.function() + .args(z.union([ z.string(), LookupAddress, LookupAddressAndStringArray ]), z.union([ z.number(), z.string() ]), z.string()) + .returns(z.boolean()); +/** + * SSRF check address function + * `(address, family, hostname) => boolean` + */ +export type SSRFCheckAddressFunction = z.infer; - dta: { - enable: true, - }, +export const SecurityMiddlewareName = z.enum([ + 'csrf', + 'hsts', + 'methodnoallow', + 'noopen', + 'nosniff', + 'csp', + 'xssProtection', + 'xframe', + 'dta', +]); +export type SecurityMiddlewareName = z.infer; - methodnoallow: { - enable: true, - }, +/** + * (ctx) => boolean + */ +const IgnoreOrMatchHandler = z.function().args(z.string()).returns(z.boolean()); +export type IgnoreOrMatchHandler = z.infer; - noopen: { - enable: true, - }, +const IgnoreOrMatch = z.union([ + z.string(), z.instanceof(RegExp), IgnoreOrMatchHandler, +]); +export type IgnoreOrMatch = z.infer; - nosniff: { - enable: true, - }, +const IgnoreOrMatchOption = z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional(); +export type IgnoreOrMatchOption = z.infer; - referrerPolicy: { - enable: false, - value: 'no-referrer-when-downgrade', - }, +/** + * security options + * @member Config#security + */ +export const SecurityConfig = z.object({ + /** + * domain white list + * + * Default to `[]` + */ + domainWhiteList: z.array(z.string()).default([]), + /** + * protocol white list + * + * Default to `[]` + */ + protocolWhiteList: z.array(z.string()).default([]), + /** + * default open security middleware + * + * Default to `'csrf,hsts,methodnoallow,noopen,nosniff,csp,xssProtection,xframe,dta'` + */ + defaultMiddleware: z.union([ z.string(), z.array(SecurityMiddlewareName) ]) + .default(SecurityMiddlewareName.options), + /** + * whether defend csrf attack + */ + csrf: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + /** + * csrf token detect source type + * + * Default to `'ctoken'` + */ + type: z.enum([ 'ctoken', 'referer', 'all', 'any' ]).default('ctoken'), + /** + * ignore json request + * + * Default to `false` + * + * @deprecated is not safe now, don't use it + */ + ignoreJSON: z.boolean().default(false), + /** + * csrf token cookie name + * + * Default to `'csrfToken'` + */ + cookieName: z.union([ z.string(), z.array(z.string()) ]).default('csrfToken'), + /** + * csrf token session name + * + * Default to `'csrfToken'` + */ + sessionName: z.string().default('csrfToken'), + /** + * csrf token request header name + * + * Default to `'x-csrf-token'` + */ + headerName: z.string().default('x-csrf-token'), + /** + * csrf token request body field name + * + * Default to `'_csrf'` + */ + bodyName: z.union([ z.string(), z.array(z.string()) ]).default('_csrf'), + /** + * csrf token request query field name + * + * Default to `'_csrf'` + */ + queryName: z.union([ z.string(), z.array(z.string()) ]).default('_csrf'), + /** + * rotate csrf token when it is invalid + * + * Default to `false` + */ + rotateWhenInvalid: z.boolean().default(false), + /** + * These config works when using `'ctoken'` type + * + * Default to `false` + */ + useSession: z.boolean().default(false), + /** + * csrf token cookie domain setting, + * can be `(ctx) => string` or `string` + * + * Default to `undefined`, auto set the cookie domain in the safe way + */ + cookieDomain: z.union([ + z.string(), + z.function() + .args(z.any()) + .returns(z.string()), + ]).optional(), + /** + * csrf token check requests config + */ + supportedRequests: z.array(CSRFSupportRequestItem) + .default([ + { path: /^\//, methods: [ 'POST', 'PATCH', 'DELETE', 'PUT', 'CONNECT' ] }, + ]), + /** + * referer or origin header white list. + * It only works when using `'referer'` type + * + * Default to `[]` + */ + refererWhiteList: z.array(z.string()).default([]), + /** + * csrf token cookie options + * + * Default to `{ + * signed: false, + * httpOnly: false, + * overwrite: true, + * }` + */ + cookieOptions: z.object({ + signed: z.boolean(), + httpOnly: z.boolean(), + overwrite: z.boolean(), + }).default({ + signed: false, + httpOnly: false, + overwrite: true, + }), + }).default({}), + /** + * whether enable X-Frame-Options response header + */ + xframe: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + /** + * X-Frame-Options value, can be `'DENY'`, `'SAMEORIGIN'`, `'ALLOW-FROM https://example.com'` + * + * Default to `'SAMEORIGIN'` + */ + value: z.string().default('SAMEORIGIN'), + }).default({}), + /** + * whether enable Strict-Transport-Security response header + */ + hsts: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `false` + */ + enable: z.boolean().default(false), + /** + * Max age of Strict-Transport-Security in seconds + * + * Default to `365 * 24 * 3600` + */ + maxAge: z.number().default(365 * 24 * 3600), + /** + * Whether include sub domains + * + * Default to `false` + */ + includeSubdomains: z.boolean().default(false), + }).default({}), + /** + * whether enable Http Method filter + */ + methodnoallow: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + }).default({}), + /** + * whether enable IE automatically download open + */ + noopen: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + }).default({}), + /** + * whether enable IE8 automatically detect mime + */ + nosniff: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + }).default({}), + /** + * whether enable IE8 XSS Filter + */ + xssProtection: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + /** + * X-XSS-Protection response header value + * + * Default to `'1; mode=block'` + */ + value: z.string().default('1; mode=block'), + }).default({}), + /** + * content security policy config + */ + csp: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `false` + */ + enable: z.boolean().default(false), + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#csp_overview + policy: z.record(z.union([ z.string(), z.array(z.string()), z.boolean() ])).default({}), + /** + * whether enable report only mode + * Default to `undefined` + */ + reportOnly: z.boolean().optional(), + /** + * whether support IE + * Default to `undefined` + */ + supportIE: z.boolean().optional(), + }).default({}), + /** + * whether enable referrer policy + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + */ + referrerPolicy: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `false` + */ + enable: z.boolean().default(false), + /** + * referrer policy value + * + * Default to `'no-referrer-when-downgrade'` + */ + value: z.string().default('no-referrer-when-downgrade'), + }).default({}), + /** + * whether enable auto avoid directory traversal attack + */ + dta: z.object({ + match: IgnoreOrMatchOption, + ignore: IgnoreOrMatchOption, + /** + * Default to `true` + */ + enable: z.boolean().default(true), + }).default({}), + ssrf: z.object({ + ipBlackList: z.array(z.string()).optional(), + ipExceptionList: z.array(z.string()).optional(), + hostnameExceptionList: z.array(z.string()).optional(), + checkAddress: SSRFCheckAddressFunction.optional(), + }).default({}), + match: z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional(), + ignore: z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional(), + __protocolWhiteListSet: z.set(z.string()).optional().readonly(), +}); +export type SecurityConfig = z.infer; - xssProtection: { - enable: true, - value: '1; mode=block', - }, +const SecurityHelperOnTagAttrHandler = z.function() + .args(z.string(), z.string(), z.string(), z.boolean()) + .returns(z.union([ z.string(), z.void() ])); - csp: { - enable: false, - policy: {}, - }, +/** + * (tag: string, name: string, value: string, isWhiteAttr: boolean) => string | void + */ +export type SecurityHelperOnTagAttrHandler = z.infer; - ssrf: { - ipBlackList: undefined, - ipExceptionList: undefined, - hostnameExceptionList: undefined, - checkAddress: undefined, - }, - } as SecurityConfig, +export const SecurityHelperConfig = z.object({ + shtml: z.object({ + /** + * tag attribute white list + */ + whiteList: z.record(z.array(z.string())).optional(), + /** + * domain white list + * @deprecated use `config.security.domainWhiteList` instead + */ + domainWhiteList: z.array(z.string()).optional(), + /** + * tag attribute handler + */ + onTagAttr: SecurityHelperOnTagAttrHandler.optional(), + }).default({}), +}); +export type SecurityHelperConfig = z.infer; - helper: { - shtml: {}, - } as SecurityHelperConfig, +export default { + security: SecurityConfig.parse({}), + helper: SecurityHelperConfig.parse({}), }; diff --git a/src/lib/extend/safe_curl.ts b/src/lib/extend/safe_curl.ts index 18f28ba..8e60f5e 100644 --- a/src/lib/extend/safe_curl.ts +++ b/src/lib/extend/safe_curl.ts @@ -1,5 +1,5 @@ import { EggCore } from '@eggjs/core'; -import { SSRFCheckAddressFunction } from '../../types.js'; +import type { SSRFCheckAddressFunction } from '../../types.js'; const SSRF_HTTPCLIENT = Symbol('SSRF_HTTPCLIENT'); diff --git a/src/lib/middlewares/index.ts b/src/lib/middlewares/index.ts index cb57547..3bcc525 100644 --- a/src/lib/middlewares/index.ts +++ b/src/lib/middlewares/index.ts @@ -1,4 +1,3 @@ -import { EggCore, MiddlewareFunc } from '@eggjs/core'; import csp from './csp.js'; import csrf from './csrf.js'; import dta from './dta.js'; @@ -21,4 +20,4 @@ export default { referrerPolicy, xframe, xssProtection, -} as Record MiddlewareFunc>; +}; diff --git a/src/types.ts b/src/types.ts index 3edad67..f98e908 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,303 +1,11 @@ -import type { LookupAddress } from 'node:dns'; -import type { Context } from '@eggjs/core'; import './app/extend/application.js'; import './app/extend/context.js'; +import type { + SecurityConfig, + SecurityHelperConfig, +} from './config/config.default.js'; -export interface CSRFSupportRequestItem { - path: RegExp; - methods: string[]; -} - -export type SSRFCheckAddressFunction = ( - addresses: string | LookupAddress | (string | LookupAddress)[], - family: number | string, - hostname: string, -) => boolean; - -/** - * security options - * @member Config#security - */ -export interface SecurityConfig { - /** - * domain white list - * - * Default to `[]` - */ - domainWhiteList: string[]; - /** - * protocol white list - * - * Default to `[]` - */ - protocolWhiteList: string[]; - /** - * default open security middleware - * - * Default to `'csrf,hsts,methodnoallow,noopen,nosniff,csp,xssProtection,xframe,dta'` - */ - defaultMiddleware: string; - /** - * whether defend csrf attack - */ - csrf: { - /** - * Default to `true` - */ - enable: boolean; - /** - * csrf token detect source type - * - * Default to `'ctoken'` - */ - type: 'ctoken' | 'referer' | 'all' | 'any'; - /** - * ignore json request - * - * Default to `false` - * - * @deprecated is not safe now, don't use it - */ - ignoreJSON: boolean; - /** - * csrf token cookie name - * - * Default to `'csrfToken'` - */ - cookieName: string | string[]; - /** - * csrf token session name - * - * Default to `'csrfToken'` - */ - sessionName: string; - /** - * csrf token request header name - * - * Default to `'x-csrf-token'` - */ - headerName: string; - /** - * csrf token request body field name - * - * Default to `'_csrf'` - */ - bodyName: string | string[]; - /** - * csrf token request query field name - * - * Default to `'_csrf'` - */ - queryName: string | string[]; - /** - * rotate csrf token when it is invalid - * - * Default to `false` - */ - rotateWhenInvalid: boolean; - /** - * These config works when using `'ctoken'` type - * - * Default to `false` - */ - useSession: boolean; - /** - * csrf token cookie domain setting, - * can be `(ctx) => string` or `string` - * - * Default to `undefined`, auto set the cookie domain in the safe way - */ - cookieDomain?: string | ((ctx: Context) => string); - /** - * csrf token check requests config - */ - supportedRequests: CSRFSupportRequestItem[]; - /** - * referer or origin header white list. - * It only works when using `'referer'` type - * - * Default to `[]` - */ - refererWhiteList: string[]; - /** - * csrf token cookie options - * - * Default to `{ - * signed: false, - * httpOnly: false, - * overwrite: true, - * }` - */ - cookieOptions: { - signed: boolean; - httpOnly: boolean; - overwrite: boolean; - }; - }; - /** - * whether enable X-Frame-Options response header - */ - xframe: { - /** - * Default to `true` - */ - enable: boolean; - /** - * X-Frame-Options value, can be `'DENY'`, `'SAMEORIGIN'`, `'ALLOW-FROM https://example.com'` - * - * Default to `'SAMEORIGIN'` - */ - value: 'DENY' | 'SAMEORIGIN' | string; - }; - /** - * whether enable Strict-Transport-Security response header - */ - hsts: { - /** - * Default to `false` - */ - enable: boolean; - /** - * Max age of Strict-Transport-Security in seconds - * - * Default to `365 * 24 * 3600` - */ - maxAge: number; - /** - * Whether include sub domains - * - * Default to `false` - */ - includeSubdomains: boolean; - }; - /** - * whether enable Http Method filter - */ - methodnoallow: { - /** - * Default to `true` - */ - enable: boolean; - }; - /** - * whether enable IE automatically download open - */ - noopen: { - /** - * Default to `true` - */ - enable: boolean; - }; - /** - * whether enable IE8 automatically detect mime - */ - nosniff: { - /** - * Default to `true` - */ - enable: boolean; - }; - /** - * whether enable IE8 XSS Filter - */ - xssProtection: { - /** - * Default to `true` - */ - enable: boolean; - /** - * X-XSS-Protection response header value - * - * Default to `'1; mode=block'` - */ - value: string; - }; - /** - * content security policy config - */ - csp: { - /** - * Default to `false` - */ - enable: boolean; - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#csp_overview - policy: Record; - /** - * whether enable report only mode - * Default to `undefined` - */ - reportOnly?: boolean; - /** - * whether support IE - * Default to `undefined` - */ - supportIE?: boolean; - // reportUri: string; - // hashAlgorithm: string; - // reportHandler: (ctx: any, reportUri: string, policy: string, violatedDirective: string, originalPolicy: string, isReportOnly: boolean) => void; - }; - /** - * whether enable referrer policy - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy - */ - referrerPolicy: { - /** - * Default to `false` - */ - enable: boolean; - /** - * referrer policy value - * - * Default to `'no-referrer-when-downgrade'` - */ - value: string; - }; - /** - * whether enable auto avoid directory traversal attack - */ - dta: { - /** - * Default to `true` - */ - enable: boolean; - }; - - ssrf: { - ipBlackList?: string[]; - ipExceptionList?: string[]; - hostnameExceptionList?: string[]; - checkAddress?: SSRFCheckAddressFunction; - }; - - match?: string | RegExp; - ignore?: string | RegExp; - - /** - * @private - */ - readonly __protocolWhiteListSet?: Set; -} - -export type SecurityHelperOnTagAttrHandler = ( - tag: string, name: string, value: string, isWhiteAttr: boolean) => string | void; - -export interface SecurityHelperConfig { - shtml: { - /** - * tag attribute white list - */ - whiteList?: Record; - /** - * domain white list - * @deprecated use `config.security.domainWhiteList` instead - */ - domainWhiteList?: string[]; - /** - * tag attribute handler - */ - onTagAttr?: SecurityHelperOnTagAttrHandler; - }; -} +export type * from './config/config.default.js'; declare module '@eggjs/core' { // add EggAppConfig overrides types diff --git a/test/config/config.default.test.ts b/test/config/config.default.test.ts new file mode 100644 index 0000000..da58197 --- /dev/null +++ b/test/config/config.default.test.ts @@ -0,0 +1,8 @@ +import snapshot from 'snap-shot-it'; +import config from '../../src/config/config.default.js'; + +describe('test/config/config.default.test.ts', () => { + it('should config default values keep stable', () => { + snapshot(config); + }); +}); diff --git a/test/csp.test.ts b/test/csp.test.ts index d7956c6..f91b4fa 100644 --- a/test/csp.test.ts +++ b/test/csp.test.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'node:assert'; import { mm, MockApplication } from '@eggjs/mock'; +import snapshot from 'snap-shot-it'; describe('test/csp.test.ts', () => { let app: MockApplication; @@ -101,6 +102,7 @@ describe('test/csp.test.ts', () => { }); it('should ignore path', async () => { + snapshot(app2.config.security); const res = await app2.httpRequest() .get('/api/update') .expect(200); diff --git a/test/fixtures/apps/csp-ignore/config/config.js b/test/fixtures/apps/csp-ignore/config/config.js index 2d2196f..81d96ef 100755 --- a/test/fixtures/apps/csp-ignore/config/config.js +++ b/test/fixtures/apps/csp-ignore/config/config.js @@ -6,7 +6,7 @@ exports.security = { defaultMiddleware: 'csp', csp:{ enable: true, - ignore:'/api/', + ignore: '/api/', policy:{ 'script-src': [ '\'self\'',