@eggjs/security 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +569 -0
- package/README.zh-CN.md +441 -0
- package/dist/commonjs/agent.d.ts +6 -0
- package/dist/commonjs/agent.js +14 -0
- package/dist/commonjs/app/extend/agent.d.ts +5 -0
- package/dist/commonjs/app/extend/agent.js +11 -0
- package/dist/commonjs/app/extend/application.d.ts +16 -0
- package/dist/commonjs/app/extend/application.js +35 -0
- package/dist/commonjs/app/extend/context.d.ts +68 -0
- package/dist/commonjs/app/extend/context.js +283 -0
- package/dist/commonjs/app/extend/helper.d.ts +12 -0
- package/dist/commonjs/app/extend/helper.js +10 -0
- package/dist/commonjs/app/extend/response.d.ts +41 -0
- package/dist/commonjs/app/extend/response.js +85 -0
- package/dist/commonjs/app/middleware/securities.d.ts +4 -0
- package/dist/commonjs/app/middleware/securities.js +55 -0
- package/dist/commonjs/app.d.ts +6 -0
- package/dist/commonjs/app.js +29 -0
- package/dist/commonjs/config/config.default.d.ts +871 -0
- package/dist/commonjs/config/config.default.js +357 -0
- package/dist/commonjs/config/config.local.d.ts +5 -0
- package/dist/commonjs/config/config.local.js +10 -0
- package/dist/commonjs/index.d.ts +1 -0
- package/dist/commonjs/index.js +14 -0
- package/dist/commonjs/lib/extend/safe_curl.d.ts +16 -0
- package/dist/commonjs/lib/extend/safe_curl.js +28 -0
- package/dist/commonjs/lib/helper/cliFilter.d.ts +4 -0
- package/dist/commonjs/lib/helper/cliFilter.js +20 -0
- package/dist/commonjs/lib/helper/escape.d.ts +2 -0
- package/dist/commonjs/lib/helper/escape.js +8 -0
- package/dist/commonjs/lib/helper/escapeShellArg.d.ts +1 -0
- package/dist/commonjs/lib/helper/escapeShellArg.js +8 -0
- package/dist/commonjs/lib/helper/escapeShellCmd.d.ts +1 -0
- package/dist/commonjs/lib/helper/escapeShellCmd.js +17 -0
- package/dist/commonjs/lib/helper/index.d.ts +21 -0
- package/dist/commonjs/lib/helper/index.js +26 -0
- package/dist/commonjs/lib/helper/shtml.d.ts +2 -0
- package/dist/commonjs/lib/helper/shtml.js +76 -0
- package/dist/commonjs/lib/helper/sjs.d.ts +4 -0
- package/dist/commonjs/lib/helper/sjs.js +52 -0
- package/dist/commonjs/lib/helper/sjson.d.ts +1 -0
- package/dist/commonjs/lib/helper/sjson.js +45 -0
- package/dist/commonjs/lib/helper/spath.d.ts +5 -0
- package/dist/commonjs/lib/helper/spath.js +28 -0
- package/dist/commonjs/lib/helper/surl.d.ts +2 -0
- package/dist/commonjs/lib/helper/surl.js +33 -0
- package/dist/commonjs/lib/middlewares/csp.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/csp.js +68 -0
- package/dist/commonjs/lib/middlewares/csrf.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/csrf.js +42 -0
- package/dist/commonjs/lib/middlewares/dta.d.ts +3 -0
- package/dist/commonjs/lib/middlewares/dta.js +14 -0
- package/dist/commonjs/lib/middlewares/hsts.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/hsts.js +23 -0
- package/dist/commonjs/lib/middlewares/index.d.ts +13 -0
- package/dist/commonjs/lib/middlewares/index.js +28 -0
- package/dist/commonjs/lib/middlewares/methodnoallow.d.ts +3 -0
- package/dist/commonjs/lib/middlewares/methodnoallow.js +22 -0
- package/dist/commonjs/lib/middlewares/noopen.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/noopen.js +17 -0
- package/dist/commonjs/lib/middlewares/nosniff.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/nosniff.js +30 -0
- package/dist/commonjs/lib/middlewares/referrerPolicy.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/referrerPolicy.js +36 -0
- package/dist/commonjs/lib/middlewares/xframe.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/xframe.js +19 -0
- package/dist/commonjs/lib/middlewares/xssProtection.d.ts +4 -0
- package/dist/commonjs/lib/middlewares/xssProtection.js +16 -0
- package/dist/commonjs/lib/utils.d.ts +19 -0
- package/dist/commonjs/lib/utils.js +206 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/types.d.ts +10 -0
- package/dist/commonjs/types.js +5 -0
- package/dist/esm/agent.d.ts +6 -0
- package/dist/esm/agent.js +11 -0
- package/dist/esm/app/extend/agent.d.ts +5 -0
- package/dist/esm/app/extend/agent.js +8 -0
- package/dist/esm/app/extend/application.d.ts +16 -0
- package/dist/esm/app/extend/application.js +32 -0
- package/dist/esm/app/extend/context.d.ts +68 -0
- package/dist/esm/app/extend/context.js +244 -0
- package/dist/esm/app/extend/helper.d.ts +12 -0
- package/dist/esm/app/extend/helper.js +5 -0
- package/dist/esm/app/extend/response.d.ts +41 -0
- package/dist/esm/app/extend/response.js +82 -0
- package/dist/esm/app/middleware/securities.d.ts +4 -0
- package/dist/esm/app/middleware/securities.js +50 -0
- package/dist/esm/app.d.ts +6 -0
- package/dist/esm/app.js +26 -0
- package/dist/esm/config/config.default.d.ts +871 -0
- package/dist/esm/config/config.default.js +351 -0
- package/dist/esm/config/config.local.d.ts +5 -0
- package/dist/esm/config/config.local.js +8 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/lib/extend/safe_curl.d.ts +16 -0
- package/dist/esm/lib/extend/safe_curl.js +25 -0
- package/dist/esm/lib/helper/cliFilter.d.ts +4 -0
- package/dist/esm/lib/helper/cliFilter.js +17 -0
- package/dist/esm/lib/helper/escape.d.ts +2 -0
- package/dist/esm/lib/helper/escape.js +3 -0
- package/dist/esm/lib/helper/escapeShellArg.d.ts +1 -0
- package/dist/esm/lib/helper/escapeShellArg.js +5 -0
- package/dist/esm/lib/helper/escapeShellCmd.d.ts +1 -0
- package/dist/esm/lib/helper/escapeShellCmd.js +14 -0
- package/dist/esm/lib/helper/index.d.ts +21 -0
- package/dist/esm/lib/helper/index.js +21 -0
- package/dist/esm/lib/helper/shtml.d.ts +2 -0
- package/dist/esm/lib/helper/shtml.js +70 -0
- package/dist/esm/lib/helper/sjs.d.ts +4 -0
- package/dist/esm/lib/helper/sjs.js +49 -0
- package/dist/esm/lib/helper/sjson.d.ts +1 -0
- package/dist/esm/lib/helper/sjson.js +39 -0
- package/dist/esm/lib/helper/spath.d.ts +5 -0
- package/dist/esm/lib/helper/spath.js +25 -0
- package/dist/esm/lib/helper/surl.d.ts +2 -0
- package/dist/esm/lib/helper/surl.js +30 -0
- package/dist/esm/lib/middlewares/csp.d.ts +4 -0
- package/dist/esm/lib/middlewares/csp.js +63 -0
- package/dist/esm/lib/middlewares/csrf.d.ts +4 -0
- package/dist/esm/lib/middlewares/csrf.js +37 -0
- package/dist/esm/lib/middlewares/dta.d.ts +3 -0
- package/dist/esm/lib/middlewares/dta.js +12 -0
- package/dist/esm/lib/middlewares/hsts.d.ts +4 -0
- package/dist/esm/lib/middlewares/hsts.js +21 -0
- package/dist/esm/lib/middlewares/index.d.ts +13 -0
- package/dist/esm/lib/middlewares/index.js +23 -0
- package/dist/esm/lib/middlewares/methodnoallow.d.ts +3 -0
- package/dist/esm/lib/middlewares/methodnoallow.js +20 -0
- package/dist/esm/lib/middlewares/noopen.d.ts +4 -0
- package/dist/esm/lib/middlewares/noopen.js +15 -0
- package/dist/esm/lib/middlewares/nosniff.d.ts +4 -0
- package/dist/esm/lib/middlewares/nosniff.js +28 -0
- package/dist/esm/lib/middlewares/referrerPolicy.d.ts +4 -0
- package/dist/esm/lib/middlewares/referrerPolicy.js +34 -0
- package/dist/esm/lib/middlewares/xframe.d.ts +4 -0
- package/dist/esm/lib/middlewares/xframe.js +17 -0
- package/dist/esm/lib/middlewares/xssProtection.d.ts +4 -0
- package/dist/esm/lib/middlewares/xssProtection.js +14 -0
- package/dist/esm/lib/utils.d.ts +19 -0
- package/dist/esm/lib/utils.js +194 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/types.js +3 -0
- package/dist/package.json +4 -0
- package/package.json +116 -0
- package/src/agent.ts +14 -0
- package/src/app/extend/agent.ts +14 -0
- package/src/app/extend/application.ts +51 -0
- package/src/app/extend/context.ts +282 -0
- package/src/app/extend/helper.ts +5 -0
- package/src/app/extend/response.ts +95 -0
- package/src/app/middleware/securities.ts +63 -0
- package/src/app.ts +31 -0
- package/src/config/config.default.ts +379 -0
- package/src/config/config.local.ts +9 -0
- package/src/index.ts +12 -0
- package/src/lib/extend/safe_curl.ts +35 -0
- package/src/lib/helper/cliFilter.ts +20 -0
- package/src/lib/helper/escape.ts +3 -0
- package/src/lib/helper/escapeShellArg.ts +4 -0
- package/src/lib/helper/escapeShellCmd.ts +16 -0
- package/src/lib/helper/index.ts +21 -0
- package/src/lib/helper/shtml.ts +77 -0
- package/src/lib/helper/sjs.ts +57 -0
- package/src/lib/helper/sjson.ts +35 -0
- package/src/lib/helper/spath.ts +27 -0
- package/src/lib/helper/surl.ts +35 -0
- package/src/lib/middlewares/csp.ts +70 -0
- package/src/lib/middlewares/csrf.ts +44 -0
- package/src/lib/middlewares/dta.ts +13 -0
- package/src/lib/middlewares/hsts.ts +24 -0
- package/src/lib/middlewares/index.ts +23 -0
- package/src/lib/middlewares/methodnoallow.ts +23 -0
- package/src/lib/middlewares/noopen.ts +18 -0
- package/src/lib/middlewares/nosniff.ts +32 -0
- package/src/lib/middlewares/referrerPolicy.ts +39 -0
- package/src/lib/middlewares/xframe.ts +20 -0
- package/src/lib/middlewares/xssProtection.ts +17 -0
- package/src/lib/utils.ts +208 -0
- package/src/types.ts +16 -0
- package/src/typings/index.d.ts +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Inclusion
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { BaseContextClass } from '@eggjs/core';
|
|
6
|
+
|
|
7
|
+
export default function pathFilter(this: BaseContextClass, path: string) {
|
|
8
|
+
if (typeof path !== 'string') return path;
|
|
9
|
+
|
|
10
|
+
const pathSource = path;
|
|
11
|
+
|
|
12
|
+
while (path.indexOf('%') !== -1) {
|
|
13
|
+
try {
|
|
14
|
+
path = decodeURIComponent(path);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
17
|
+
// Not a PROD env, logging with a warning.
|
|
18
|
+
this.ctx.coreLogger.warn('[@eggjs/security/lib/helper/spath] : decode file path %j failed.', path);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (path.indexOf('..') !== -1 || path[0] === '/') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return pathSource;
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BaseContextClass } from '@eggjs/core';
|
|
2
|
+
|
|
3
|
+
const escapeMap: Record<string, string> = {
|
|
4
|
+
'"': '"',
|
|
5
|
+
'<': '<',
|
|
6
|
+
'>': '>',
|
|
7
|
+
'\'': ''',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function surl(this: BaseContextClass, val: string) {
|
|
11
|
+
// Just get the converted the protocolWhiteList in `Set` mode,
|
|
12
|
+
// Avoid conversions in `foreach`
|
|
13
|
+
const protocolWhiteListSet = this.app.config.security.__protocolWhiteListSet!;
|
|
14
|
+
|
|
15
|
+
if (typeof val !== 'string') {
|
|
16
|
+
return val;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// only test on absolute path
|
|
20
|
+
if (val[0] !== '/') {
|
|
21
|
+
const arr = val.split('://', 2);
|
|
22
|
+
const protocol = arr.length > 1 ? arr[0].toLowerCase() : '';
|
|
23
|
+
if (protocol === '' || !protocolWhiteListSet.has(protocol)) {
|
|
24
|
+
if (this.app.config.env === 'local') {
|
|
25
|
+
this.ctx.coreLogger.warn('[@eggjs/security/surl] url: %j, protocol: %j, ' +
|
|
26
|
+
'protocol is empty or not in white list, convert to empty string', val, protocol);
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return val.replace(/["'<>]/g, ch => {
|
|
33
|
+
return escapeMap[ch];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import extend from 'extend';
|
|
2
|
+
import type { Context, Next } from '@eggjs/core';
|
|
3
|
+
import { checkIfIgnore } from '../utils.js';
|
|
4
|
+
import type { SecurityConfig } from '../../types.js';
|
|
5
|
+
|
|
6
|
+
const HEADER = [
|
|
7
|
+
'x-content-security-policy',
|
|
8
|
+
'content-security-policy',
|
|
9
|
+
];
|
|
10
|
+
const REPORT_ONLY_HEADER = [
|
|
11
|
+
'x-content-security-policy-report-only',
|
|
12
|
+
'content-security-policy-report-only',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
|
|
16
|
+
const MSIE_REGEXP = / MSIE /i;
|
|
17
|
+
|
|
18
|
+
export default (options: SecurityConfig['csp']) => {
|
|
19
|
+
return async function csp(ctx: Context, next: Next) {
|
|
20
|
+
await next();
|
|
21
|
+
|
|
22
|
+
const opts = {
|
|
23
|
+
...options,
|
|
24
|
+
...ctx.securityOptions.csp,
|
|
25
|
+
};
|
|
26
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
27
|
+
|
|
28
|
+
let finalHeader;
|
|
29
|
+
const matchedOption = extend(true, {}, opts.policy);
|
|
30
|
+
const bufArray = [];
|
|
31
|
+
|
|
32
|
+
const headers = opts.reportOnly ? REPORT_ONLY_HEADER : HEADER;
|
|
33
|
+
if (opts.supportIE && MSIE_REGEXP.test(ctx.get('user-agent'))) {
|
|
34
|
+
finalHeader = headers[0];
|
|
35
|
+
} else {
|
|
36
|
+
finalHeader = headers[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const key in matchedOption) {
|
|
40
|
+
const value = matchedOption[key];
|
|
41
|
+
// Other arrays are splitted into strings EXCEPT `sandbox`
|
|
42
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
|
|
43
|
+
if (key === 'sandbox' && value === true) {
|
|
44
|
+
bufArray.push(key);
|
|
45
|
+
} else {
|
|
46
|
+
let values = (Array.isArray(value) ? value : [ value ]) as string[];
|
|
47
|
+
if (key === 'script-src') {
|
|
48
|
+
const hasNonce = values.some(function(val) {
|
|
49
|
+
return val.indexOf('nonce-') !== -1;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!hasNonce) {
|
|
53
|
+
values.push('\'nonce-' + ctx.nonce + '\'');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
values = values.map(function(d) {
|
|
58
|
+
if (d.startsWith('.')) {
|
|
59
|
+
d = '*' + d;
|
|
60
|
+
}
|
|
61
|
+
return d;
|
|
62
|
+
});
|
|
63
|
+
bufArray.push(key + ' ' + values.join(' '));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const headerString = bufArray.join(';');
|
|
67
|
+
ctx.set(finalHeader, headerString);
|
|
68
|
+
ctx.set('x-csp-nonce', ctx.nonce);
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { debuglog } from 'node:util';
|
|
2
|
+
import type { Context, Next } from '@eggjs/core';
|
|
3
|
+
import typeis from 'type-is';
|
|
4
|
+
import { checkIfIgnore } from '../utils.js';
|
|
5
|
+
import type { SecurityConfig } from '../../types.js';
|
|
6
|
+
|
|
7
|
+
const debug = debuglog('@eggjs/security/lib/middlewares/csrf');
|
|
8
|
+
|
|
9
|
+
export default (options: SecurityConfig['csrf']) => {
|
|
10
|
+
return function csrf(ctx: Context, next: Next) {
|
|
11
|
+
if (checkIfIgnore(options, ctx)) {
|
|
12
|
+
return next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ensure csrf token exists
|
|
16
|
+
if ([ 'any', 'all', 'ctoken' ].includes(options.type)) {
|
|
17
|
+
ctx.ensureCsrfSecret();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// supported requests
|
|
21
|
+
const method = ctx.method;
|
|
22
|
+
let isSupported = false;
|
|
23
|
+
for (const eachRule of options.supportedRequests) {
|
|
24
|
+
if (eachRule.path.test(ctx.path)) {
|
|
25
|
+
if (eachRule.methods.includes(method)) {
|
|
26
|
+
isSupported = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!isSupported) {
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.ignoreJSON && typeis.is(ctx.get('content-type'), 'json')) {
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const body = ctx.request.body;
|
|
40
|
+
debug('%s %s, got %j', ctx.method, ctx.url, body);
|
|
41
|
+
ctx.assertCsrf();
|
|
42
|
+
return next();
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { isSafePath } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
// https://en.wikipedia.org/wiki/Directory_traversal_attack
|
|
5
|
+
export default () => {
|
|
6
|
+
return function dta(ctx: Context, next: Next) {
|
|
7
|
+
const path = ctx.path;
|
|
8
|
+
if (!isSafePath(path, ctx)) {
|
|
9
|
+
ctx.throw(400);
|
|
10
|
+
}
|
|
11
|
+
return next();
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
// Set Strict-Transport-Security header
|
|
6
|
+
export default (options: SecurityConfig['hsts']) => {
|
|
7
|
+
return async function hsts(ctx: Context, next: Next) {
|
|
8
|
+
await next();
|
|
9
|
+
|
|
10
|
+
const opts = {
|
|
11
|
+
...options,
|
|
12
|
+
...ctx.securityOptions.hsts,
|
|
13
|
+
};
|
|
14
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
15
|
+
|
|
16
|
+
let val = 'max-age=' + opts.maxAge;
|
|
17
|
+
// If opts.includeSubdomains is defined,
|
|
18
|
+
// the rule is also valid for all the sub domains of the website
|
|
19
|
+
if (opts.includeSubdomains) {
|
|
20
|
+
val += '; includeSubdomains';
|
|
21
|
+
}
|
|
22
|
+
ctx.set('strict-transport-security', val);
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import csp from './csp.js';
|
|
2
|
+
import csrf from './csrf.js';
|
|
3
|
+
import dta from './dta.js';
|
|
4
|
+
import hsts from './hsts.js';
|
|
5
|
+
import methodnoallow from './methodnoallow.js';
|
|
6
|
+
import noopen from './noopen.js';
|
|
7
|
+
import nosniff from './nosniff.js';
|
|
8
|
+
import referrerPolicy from './referrerPolicy.js';
|
|
9
|
+
import xframe from './xframe.js';
|
|
10
|
+
import xssProtection from './xssProtection.js';
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
csp,
|
|
14
|
+
csrf,
|
|
15
|
+
dta,
|
|
16
|
+
hsts,
|
|
17
|
+
methodnoallow,
|
|
18
|
+
noopen,
|
|
19
|
+
nosniff,
|
|
20
|
+
referrerPolicy,
|
|
21
|
+
xframe,
|
|
22
|
+
xssProtection,
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { METHODS } from 'node:http';
|
|
2
|
+
import type { Context, Next } from '@eggjs/core';
|
|
3
|
+
|
|
4
|
+
const METHODS_NOT_ALLOWED = [ 'TRACE', 'TRACK' ];
|
|
5
|
+
const safeHttpMethodsMap: Record<string, boolean> = {};
|
|
6
|
+
|
|
7
|
+
for (const method of METHODS) {
|
|
8
|
+
if (!METHODS_NOT_ALLOWED.includes(method)) {
|
|
9
|
+
safeHttpMethodsMap[method.toUpperCase()] = true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// https://www.owasp.org/index.php/Cross_Site_Tracing
|
|
14
|
+
// http://jsperf.com/find-by-map-with-find-by-array
|
|
15
|
+
export default () => {
|
|
16
|
+
return function notAllow(ctx: Context, next: Next) {
|
|
17
|
+
// ctx.method is upper case
|
|
18
|
+
if (!safeHttpMethodsMap[ctx.method]) {
|
|
19
|
+
ctx.throw(405);
|
|
20
|
+
}
|
|
21
|
+
return next();
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
// @see http://blogs.msdn.com/b/ieinternals/archive/2009/06/30/internet-explorer-custom-http-headers.aspx
|
|
6
|
+
export default (options: SecurityConfig['noopen']) => {
|
|
7
|
+
return async function noopen(ctx: Context, next: Next) {
|
|
8
|
+
await next();
|
|
9
|
+
|
|
10
|
+
const opts = {
|
|
11
|
+
...options,
|
|
12
|
+
...ctx.securityOptions.noopen,
|
|
13
|
+
};
|
|
14
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
15
|
+
|
|
16
|
+
ctx.set('x-download-options', 'noopen');
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
// status codes for redirects
|
|
6
|
+
// @see https://github.com/jshttp/statuses/blob/master/index.js#L33
|
|
7
|
+
const RedirectStatus: Record<number, boolean> = {
|
|
8
|
+
300: true,
|
|
9
|
+
301: true,
|
|
10
|
+
302: true,
|
|
11
|
+
303: true,
|
|
12
|
+
305: true,
|
|
13
|
+
307: true,
|
|
14
|
+
308: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default (options: SecurityConfig['nosniff']) => {
|
|
18
|
+
return async function nosniff(ctx: Context, next: Next) {
|
|
19
|
+
await next();
|
|
20
|
+
|
|
21
|
+
// ignore redirect response
|
|
22
|
+
if (RedirectStatus[ctx.status]) return;
|
|
23
|
+
|
|
24
|
+
const opts = {
|
|
25
|
+
...options,
|
|
26
|
+
...ctx.securityOptions.nosniff,
|
|
27
|
+
};
|
|
28
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
29
|
+
|
|
30
|
+
ctx.set('x-content-type-options', 'nosniff');
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy
|
|
6
|
+
const ALLOWED_POLICIES_ENUM = [
|
|
7
|
+
'no-referrer',
|
|
8
|
+
'no-referrer-when-downgrade',
|
|
9
|
+
'origin',
|
|
10
|
+
'origin-when-cross-origin',
|
|
11
|
+
'same-origin',
|
|
12
|
+
'strict-origin',
|
|
13
|
+
'strict-origin-when-cross-origin',
|
|
14
|
+
'unsafe-url',
|
|
15
|
+
'',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export default (options: SecurityConfig['referrerPolicy']) => {
|
|
19
|
+
return async function referrerPolicy(ctx: Context, next: Next) {
|
|
20
|
+
await next();
|
|
21
|
+
|
|
22
|
+
const opts = {
|
|
23
|
+
...options,
|
|
24
|
+
// check refererPolicy for backward compatibility
|
|
25
|
+
// typo on the old version
|
|
26
|
+
// @see https://github.com/eggjs/security/blob/e3408408adec5f8d009d37f75126ed082481d0ac/lib/middlewares/referrerPolicy.js#L21C59-L21C72
|
|
27
|
+
...(ctx.securityOptions as any).refererPolicy,
|
|
28
|
+
...ctx.securityOptions.referrerPolicy,
|
|
29
|
+
};
|
|
30
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
31
|
+
|
|
32
|
+
const policy = opts.value;
|
|
33
|
+
if (!ALLOWED_POLICIES_ENUM.includes(policy)) {
|
|
34
|
+
throw new Error('"' + policy + '" is not available.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctx.set('referrer-policy', policy);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export default (options: SecurityConfig['xframe']) => {
|
|
6
|
+
return async function xframe(ctx: Context, next: Next) {
|
|
7
|
+
await next();
|
|
8
|
+
|
|
9
|
+
const opts = {
|
|
10
|
+
...options,
|
|
11
|
+
...ctx.securityOptions.xframe,
|
|
12
|
+
};
|
|
13
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
14
|
+
|
|
15
|
+
// DENY, SAMEORIGIN, ALLOW-FROM
|
|
16
|
+
// https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options?redirectlocale=en-US&redirectslug=The_X-FRAME-OPTIONS_response_header
|
|
17
|
+
const value = opts.value || 'SAMEORIGIN';
|
|
18
|
+
ctx.set('x-frame-options', value);
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Context, Next } from '@eggjs/core';
|
|
2
|
+
import { checkIfIgnore } from '../utils.js';
|
|
3
|
+
import type { SecurityConfig } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export default (options: SecurityConfig['xssProtection']) => {
|
|
6
|
+
return async function xssProtection(ctx: Context, next: Next) {
|
|
7
|
+
await next();
|
|
8
|
+
|
|
9
|
+
const opts = {
|
|
10
|
+
...options,
|
|
11
|
+
...ctx.securityOptions.xssProtection,
|
|
12
|
+
};
|
|
13
|
+
if (checkIfIgnore(opts, ctx)) return;
|
|
14
|
+
|
|
15
|
+
ctx.set('x-xss-protection', opts.value);
|
|
16
|
+
};
|
|
17
|
+
};
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { normalize } from 'node:path';
|
|
2
|
+
import matcher from 'matcher';
|
|
3
|
+
import IP from '@eggjs/ip';
|
|
4
|
+
import { Context } from '@eggjs/core';
|
|
5
|
+
import type { PathMatchingFun } from 'egg-path-matching';
|
|
6
|
+
import { SecurityConfig } from '../types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check whether a domain is in the safe domain white list or not.
|
|
10
|
+
* @param {String} domain The inputted domain.
|
|
11
|
+
* @param {Array<string>} whiteList The white list for domain.
|
|
12
|
+
* @return {Boolean} If the `domain` is in the white list, return true; otherwise false.
|
|
13
|
+
*/
|
|
14
|
+
export function isSafeDomain(domain: string, whiteList: string[]): boolean {
|
|
15
|
+
// domain must be string, otherwise return false
|
|
16
|
+
if (typeof domain !== 'string') return false;
|
|
17
|
+
// Ignore case sensitive first
|
|
18
|
+
domain = domain.toLowerCase();
|
|
19
|
+
// add prefix `.`, because all domains in white list start with `.`
|
|
20
|
+
const hostname = '.' + domain;
|
|
21
|
+
|
|
22
|
+
return whiteList.some(rule => {
|
|
23
|
+
// Check whether we've got '*' as a wild character symbol
|
|
24
|
+
if (rule.includes('*')) {
|
|
25
|
+
return matcher.isMatch(domain, rule);
|
|
26
|
+
}
|
|
27
|
+
// If domain is an absolute path such as `http://...`
|
|
28
|
+
// We can directly check whether it directly equals to `domain`
|
|
29
|
+
// And we don't need to cope with `endWith`.
|
|
30
|
+
if (domain === rule) return true;
|
|
31
|
+
// ensure wwweggjs.com not match eggjs.com
|
|
32
|
+
if (!/^\./.test(rule)) rule = `.${rule}`;
|
|
33
|
+
return hostname.endsWith(rule);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isSafePath(path: string, ctx: Context) {
|
|
38
|
+
path = '.' + path;
|
|
39
|
+
if (path.includes('%')) {
|
|
40
|
+
try {
|
|
41
|
+
path = decodeURIComponent(path);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (ctx.app.config.env === 'local' || ctx.app.config.env === 'unittest') {
|
|
44
|
+
// not under production environment, output log
|
|
45
|
+
ctx.coreLogger.warn('[@eggjs/security: dta global block] : decode file path %j failed.', path);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const normalizePath = normalize(path);
|
|
50
|
+
return !(normalizePath.startsWith('../') || normalizePath.startsWith('..\\'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function checkIfIgnore(opts: { enable: boolean; matching?: PathMatchingFun; }, ctx: Context) {
|
|
54
|
+
// check opts.enable first
|
|
55
|
+
if (!opts.enable) return true;
|
|
56
|
+
return !opts.matching?.(ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const IP_RE = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
60
|
+
const topDomains: Record<string, number> = {};
|
|
61
|
+
[
|
|
62
|
+
'.net.cn', '.gov.cn', '.org.cn', '.com.cn',
|
|
63
|
+
].forEach(item => {
|
|
64
|
+
topDomains[item] = 2 - item.split('.').length;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function getCookieDomain(hostname: string) {
|
|
68
|
+
// TODO(fengmk2): support ipv6
|
|
69
|
+
if (IP_RE.test(hostname)) {
|
|
70
|
+
return hostname;
|
|
71
|
+
}
|
|
72
|
+
// app.test.domain.com => .test.domain.com
|
|
73
|
+
// app.stable.domain.com => .domain.com
|
|
74
|
+
// app.domain.com => .domain.com
|
|
75
|
+
// domain=.domain.com;
|
|
76
|
+
const splits = hostname.split('.');
|
|
77
|
+
let index = -2;
|
|
78
|
+
|
|
79
|
+
// only when `*.test.*.com` set `.test.*.com`
|
|
80
|
+
if (splits.length >= 4 && splits[splits.length - 3] === 'test') {
|
|
81
|
+
index = -3;
|
|
82
|
+
}
|
|
83
|
+
let domain = getDomain(splits, index);
|
|
84
|
+
if (topDomains[domain]) {
|
|
85
|
+
// app.foo.org.cn => .foo.org.cn
|
|
86
|
+
domain = getDomain(splits, index + topDomains[domain]);
|
|
87
|
+
}
|
|
88
|
+
return domain;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getDomain(splits: string[], index: number) {
|
|
92
|
+
return '.' + splits.slice(index).join('.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function merge(origin: Record<string, any>, opts?: Record<string, any>) {
|
|
96
|
+
if (!opts) {
|
|
97
|
+
return origin;
|
|
98
|
+
}
|
|
99
|
+
const res: Record<string, any> = {};
|
|
100
|
+
|
|
101
|
+
const originKeys = Object.keys(origin);
|
|
102
|
+
for (let i = 0; i < originKeys.length; i++) {
|
|
103
|
+
const key = originKeys[i];
|
|
104
|
+
res[key] = origin[key];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const keys = Object.keys(opts);
|
|
108
|
+
for (let i = 0; i < keys.length; i++) {
|
|
109
|
+
const key = keys[i];
|
|
110
|
+
res[key] = opts[key];
|
|
111
|
+
}
|
|
112
|
+
return res;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function preprocessConfig(config: SecurityConfig) {
|
|
116
|
+
// transfer ssrf.ipBlackList to ssrf.checkAddress
|
|
117
|
+
// ssrf.ipExceptionList can easily pick out unwanted ips from ipBlackList
|
|
118
|
+
// checkAddress has higher priority than ipBlackList
|
|
119
|
+
const ssrf = config.ssrf;
|
|
120
|
+
if (ssrf && ssrf.ipBlackList && !ssrf.checkAddress) {
|
|
121
|
+
const blackList = ssrf.ipBlackList.map(getContains);
|
|
122
|
+
const exceptionList = (ssrf.ipExceptionList || []).map(getContains);
|
|
123
|
+
const hostnameExceptionList = ssrf.hostnameExceptionList;
|
|
124
|
+
ssrf.checkAddress = (ipAddresses, _family, hostname) => {
|
|
125
|
+
// Check white hostname first
|
|
126
|
+
if (hostname && hostnameExceptionList) {
|
|
127
|
+
if (hostnameExceptionList.includes(hostname)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ipAddresses will be array address on Node.js >= 20
|
|
132
|
+
// [
|
|
133
|
+
// { address: '220.181.125.241', family: 4 },
|
|
134
|
+
// { address: '240e:964:ea02:b00:3::3ec', family: 6 }
|
|
135
|
+
// ]
|
|
136
|
+
if (!Array.isArray(ipAddresses)) {
|
|
137
|
+
ipAddresses = [ ipAddresses ];
|
|
138
|
+
}
|
|
139
|
+
for (const ipAddress of ipAddresses) {
|
|
140
|
+
let address: string;
|
|
141
|
+
if (typeof ipAddress === 'string') {
|
|
142
|
+
address = ipAddress;
|
|
143
|
+
} else {
|
|
144
|
+
// FIXME: should support ipv6
|
|
145
|
+
if (ipAddress.family === 6) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
address = ipAddress.address;
|
|
149
|
+
}
|
|
150
|
+
// check white list first
|
|
151
|
+
for (const exception of exceptionList) {
|
|
152
|
+
if (exception(address)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// check black list
|
|
157
|
+
for (const contains of blackList) {
|
|
158
|
+
if (contains(address)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// default allow
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Make sure that `whiteList` or `protocolWhiteList` is case insensitive
|
|
169
|
+
config.domainWhiteList = config.domainWhiteList || [];
|
|
170
|
+
config.domainWhiteList = config.domainWhiteList.map((domain: string) => domain.toLowerCase());
|
|
171
|
+
|
|
172
|
+
config.protocolWhiteList = config.protocolWhiteList || [];
|
|
173
|
+
config.protocolWhiteList = config.protocolWhiteList.map((protocol: string) => protocol.toLowerCase());
|
|
174
|
+
|
|
175
|
+
// Make sure refererWhiteList is case insensitive
|
|
176
|
+
if (config.csrf && config.csrf.refererWhiteList) {
|
|
177
|
+
config.csrf.refererWhiteList = config.csrf.refererWhiteList.map((ref: string) => ref.toLowerCase());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Directly converted to Set collection by a private property (not documented),
|
|
181
|
+
// And we NO LONGER need to do conversion in `foreach` again and again in `lib/helper/surl.ts`.
|
|
182
|
+
const protocolWhiteListSet = new Set(config.protocolWhiteList);
|
|
183
|
+
protocolWhiteListSet.add('http');
|
|
184
|
+
protocolWhiteListSet.add('https');
|
|
185
|
+
protocolWhiteListSet.add('file');
|
|
186
|
+
protocolWhiteListSet.add('data');
|
|
187
|
+
|
|
188
|
+
Object.defineProperty(config, '__protocolWhiteListSet', {
|
|
189
|
+
value: protocolWhiteListSet,
|
|
190
|
+
enumerable: false,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function getFromUrl(url: string, prop?: string): string | null {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(url);
|
|
197
|
+
return prop ? Reflect.get(parsed, prop) : parsed;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getContains(ip: string) {
|
|
204
|
+
if (IP.isV4Format(ip) || IP.isV6Format(ip)) {
|
|
205
|
+
return (address: string) => address === ip;
|
|
206
|
+
}
|
|
207
|
+
return IP.cidrSubnet(ip).contains;
|
|
208
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import './app/extend/application.js';
|
|
2
|
+
import './app/extend/context.js';
|
|
3
|
+
import type {
|
|
4
|
+
SecurityConfig,
|
|
5
|
+
SecurityHelperConfig,
|
|
6
|
+
} from './config/config.default.js';
|
|
7
|
+
|
|
8
|
+
export type * from './config/config.default.js';
|
|
9
|
+
|
|
10
|
+
declare module '@eggjs/core' {
|
|
11
|
+
// add EggAppConfig overrides types
|
|
12
|
+
interface EggAppConfig {
|
|
13
|
+
security: SecurityConfig;
|
|
14
|
+
helper: SecurityHelperConfig;
|
|
15
|
+
}
|
|
16
|
+
}
|