@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,379 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Context } from '@eggjs/core';
|
|
3
|
+
|
|
4
|
+
const CSRFSupportRequestItem = z.object({
|
|
5
|
+
path: z.instanceof(RegExp),
|
|
6
|
+
methods: z.array(z.string()),
|
|
7
|
+
});
|
|
8
|
+
export type CSRFSupportRequestItem = z.infer<typeof CSRFSupportRequestItem>;
|
|
9
|
+
|
|
10
|
+
export const LookupAddress = z.object({
|
|
11
|
+
address: z.string(),
|
|
12
|
+
family: z.number(),
|
|
13
|
+
});
|
|
14
|
+
export type LookupAddress = z.infer<typeof LookupAddress>;
|
|
15
|
+
|
|
16
|
+
const LookupAddressAndStringArray = z.union([ z.string(), LookupAddress ]).array();
|
|
17
|
+
const SSRFCheckAddressFunction = z.function()
|
|
18
|
+
.args(z.union([ z.string(), LookupAddress, LookupAddressAndStringArray ]), z.union([ z.number(), z.string() ]), z.string())
|
|
19
|
+
.returns(z.boolean());
|
|
20
|
+
/**
|
|
21
|
+
* SSRF check address function
|
|
22
|
+
* `(address, family, hostname) => boolean`
|
|
23
|
+
*/
|
|
24
|
+
export type SSRFCheckAddressFunction = z.infer<typeof SSRFCheckAddressFunction>;
|
|
25
|
+
|
|
26
|
+
export const SecurityMiddlewareName = z.enum([
|
|
27
|
+
'csrf',
|
|
28
|
+
'hsts',
|
|
29
|
+
'methodnoallow',
|
|
30
|
+
'noopen',
|
|
31
|
+
'nosniff',
|
|
32
|
+
'csp',
|
|
33
|
+
'xssProtection',
|
|
34
|
+
'xframe',
|
|
35
|
+
'dta',
|
|
36
|
+
]);
|
|
37
|
+
export type SecurityMiddlewareName = z.infer<typeof SecurityMiddlewareName>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* (ctx) => boolean
|
|
41
|
+
*/
|
|
42
|
+
const IgnoreOrMatchHandler = z.function().args(z.instanceof(Context)).returns(z.boolean());
|
|
43
|
+
export type IgnoreOrMatchHandler = z.infer<typeof IgnoreOrMatchHandler>;
|
|
44
|
+
|
|
45
|
+
const IgnoreOrMatch = z.union([
|
|
46
|
+
z.string(), z.instanceof(RegExp), IgnoreOrMatchHandler,
|
|
47
|
+
]);
|
|
48
|
+
export type IgnoreOrMatch = z.infer<typeof IgnoreOrMatch>;
|
|
49
|
+
|
|
50
|
+
const IgnoreOrMatchOption = z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional();
|
|
51
|
+
export type IgnoreOrMatchOption = z.infer<typeof IgnoreOrMatchOption>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* security options
|
|
55
|
+
* @member Config#security
|
|
56
|
+
*/
|
|
57
|
+
export const SecurityConfig = z.object({
|
|
58
|
+
/**
|
|
59
|
+
* domain white list
|
|
60
|
+
*
|
|
61
|
+
* Default to `[]`
|
|
62
|
+
*/
|
|
63
|
+
domainWhiteList: z.array(z.string()).default([]),
|
|
64
|
+
/**
|
|
65
|
+
* protocol white list
|
|
66
|
+
*
|
|
67
|
+
* Default to `[]`
|
|
68
|
+
*/
|
|
69
|
+
protocolWhiteList: z.array(z.string()).default([]),
|
|
70
|
+
/**
|
|
71
|
+
* default open security middleware
|
|
72
|
+
*
|
|
73
|
+
* Default to `'csrf,hsts,methodnoallow,noopen,nosniff,csp,xssProtection,xframe,dta'`
|
|
74
|
+
*/
|
|
75
|
+
defaultMiddleware: z.union([ z.string(), z.array(SecurityMiddlewareName) ])
|
|
76
|
+
.default(SecurityMiddlewareName.options),
|
|
77
|
+
/**
|
|
78
|
+
* whether defend csrf attack
|
|
79
|
+
*/
|
|
80
|
+
csrf: z.preprocess(val => {
|
|
81
|
+
// transform old config, `csrf: false` to `csrf: { enable: false }`
|
|
82
|
+
if (typeof val === 'boolean') {
|
|
83
|
+
return { enable: val };
|
|
84
|
+
}
|
|
85
|
+
return val;
|
|
86
|
+
}, z.object({
|
|
87
|
+
match: IgnoreOrMatchOption,
|
|
88
|
+
ignore: IgnoreOrMatchOption,
|
|
89
|
+
/**
|
|
90
|
+
* Default to `true`
|
|
91
|
+
*/
|
|
92
|
+
enable: z.boolean().default(true),
|
|
93
|
+
/**
|
|
94
|
+
* csrf token detect source type
|
|
95
|
+
*
|
|
96
|
+
* Default to `'ctoken'`
|
|
97
|
+
*/
|
|
98
|
+
type: z.enum([ 'ctoken', 'referer', 'all', 'any' ]).default('ctoken'),
|
|
99
|
+
/**
|
|
100
|
+
* ignore json request
|
|
101
|
+
*
|
|
102
|
+
* Default to `false`
|
|
103
|
+
*
|
|
104
|
+
* @deprecated is not safe now, don't use it
|
|
105
|
+
*/
|
|
106
|
+
ignoreJSON: z.boolean().default(false),
|
|
107
|
+
/**
|
|
108
|
+
* csrf token cookie name
|
|
109
|
+
*
|
|
110
|
+
* Default to `'csrfToken'`
|
|
111
|
+
*/
|
|
112
|
+
cookieName: z.union([ z.string(), z.array(z.string()) ]).default('csrfToken'),
|
|
113
|
+
/**
|
|
114
|
+
* csrf token session name
|
|
115
|
+
*
|
|
116
|
+
* Default to `'csrfToken'`
|
|
117
|
+
*/
|
|
118
|
+
sessionName: z.string().default('csrfToken'),
|
|
119
|
+
/**
|
|
120
|
+
* csrf token request header name
|
|
121
|
+
*
|
|
122
|
+
* Default to `'x-csrf-token'`
|
|
123
|
+
*/
|
|
124
|
+
headerName: z.string().default('x-csrf-token'),
|
|
125
|
+
/**
|
|
126
|
+
* csrf token request body field name
|
|
127
|
+
*
|
|
128
|
+
* Default to `'_csrf'`
|
|
129
|
+
*/
|
|
130
|
+
bodyName: z.union([ z.string(), z.array(z.string()) ]).default('_csrf'),
|
|
131
|
+
/**
|
|
132
|
+
* csrf token request query field name
|
|
133
|
+
*
|
|
134
|
+
* Default to `'_csrf'`
|
|
135
|
+
*/
|
|
136
|
+
queryName: z.union([ z.string(), z.array(z.string()) ]).default('_csrf'),
|
|
137
|
+
/**
|
|
138
|
+
* rotate csrf token when it is invalid
|
|
139
|
+
*
|
|
140
|
+
* Default to `false`
|
|
141
|
+
*/
|
|
142
|
+
rotateWhenInvalid: z.boolean().default(false),
|
|
143
|
+
/**
|
|
144
|
+
* These config works when using `'ctoken'` type
|
|
145
|
+
*
|
|
146
|
+
* Default to `false`
|
|
147
|
+
*/
|
|
148
|
+
useSession: z.boolean().default(false),
|
|
149
|
+
/**
|
|
150
|
+
* csrf token cookie domain setting,
|
|
151
|
+
* can be `(ctx) => string` or `string`
|
|
152
|
+
*
|
|
153
|
+
* Default to `undefined`, auto set the cookie domain in the safe way
|
|
154
|
+
*/
|
|
155
|
+
cookieDomain: z.union([
|
|
156
|
+
z.string(),
|
|
157
|
+
z.function()
|
|
158
|
+
.args(z.instanceof(Context))
|
|
159
|
+
.returns(z.string()),
|
|
160
|
+
]).optional(),
|
|
161
|
+
/**
|
|
162
|
+
* csrf token check requests config
|
|
163
|
+
*/
|
|
164
|
+
supportedRequests: z.array(CSRFSupportRequestItem)
|
|
165
|
+
.default([
|
|
166
|
+
{ path: /^\//, methods: [ 'POST', 'PATCH', 'DELETE', 'PUT', 'CONNECT' ] },
|
|
167
|
+
]),
|
|
168
|
+
/**
|
|
169
|
+
* referer or origin header white list.
|
|
170
|
+
* It only works when using `'referer'` type
|
|
171
|
+
*
|
|
172
|
+
* Default to `[]`
|
|
173
|
+
*/
|
|
174
|
+
refererWhiteList: z.array(z.string()).default([]),
|
|
175
|
+
/**
|
|
176
|
+
* csrf token cookie options
|
|
177
|
+
*
|
|
178
|
+
* Default to `{
|
|
179
|
+
* signed: false,
|
|
180
|
+
* httpOnly: false,
|
|
181
|
+
* overwrite: true,
|
|
182
|
+
* }`
|
|
183
|
+
*/
|
|
184
|
+
cookieOptions: z.object({
|
|
185
|
+
signed: z.boolean(),
|
|
186
|
+
httpOnly: z.boolean(),
|
|
187
|
+
overwrite: z.boolean(),
|
|
188
|
+
}).default({
|
|
189
|
+
signed: false,
|
|
190
|
+
httpOnly: false,
|
|
191
|
+
overwrite: true,
|
|
192
|
+
}),
|
|
193
|
+
}).default({})),
|
|
194
|
+
/**
|
|
195
|
+
* whether enable X-Frame-Options response header
|
|
196
|
+
*/
|
|
197
|
+
xframe: z.object({
|
|
198
|
+
match: IgnoreOrMatchOption,
|
|
199
|
+
ignore: IgnoreOrMatchOption,
|
|
200
|
+
/**
|
|
201
|
+
* Default to `true`
|
|
202
|
+
*/
|
|
203
|
+
enable: z.boolean().default(true),
|
|
204
|
+
/**
|
|
205
|
+
* X-Frame-Options value, can be `'DENY'`, `'SAMEORIGIN'`, `'ALLOW-FROM https://example.com'`
|
|
206
|
+
*
|
|
207
|
+
* Default to `'SAMEORIGIN'`
|
|
208
|
+
*/
|
|
209
|
+
value: z.string().default('SAMEORIGIN'),
|
|
210
|
+
}).default({}),
|
|
211
|
+
/**
|
|
212
|
+
* whether enable Strict-Transport-Security response header
|
|
213
|
+
*/
|
|
214
|
+
hsts: z.object({
|
|
215
|
+
match: IgnoreOrMatchOption,
|
|
216
|
+
ignore: IgnoreOrMatchOption,
|
|
217
|
+
/**
|
|
218
|
+
* Default to `false`
|
|
219
|
+
*/
|
|
220
|
+
enable: z.boolean().default(false),
|
|
221
|
+
/**
|
|
222
|
+
* Max age of Strict-Transport-Security in seconds
|
|
223
|
+
*
|
|
224
|
+
* Default to `365 * 24 * 3600`
|
|
225
|
+
*/
|
|
226
|
+
maxAge: z.number().default(365 * 24 * 3600),
|
|
227
|
+
/**
|
|
228
|
+
* Whether include sub domains
|
|
229
|
+
*
|
|
230
|
+
* Default to `false`
|
|
231
|
+
*/
|
|
232
|
+
includeSubdomains: z.boolean().default(false),
|
|
233
|
+
}).default({}),
|
|
234
|
+
/**
|
|
235
|
+
* whether enable Http Method filter
|
|
236
|
+
*/
|
|
237
|
+
methodnoallow: z.object({
|
|
238
|
+
match: IgnoreOrMatchOption,
|
|
239
|
+
ignore: IgnoreOrMatchOption,
|
|
240
|
+
/**
|
|
241
|
+
* Default to `true`
|
|
242
|
+
*/
|
|
243
|
+
enable: z.boolean().default(true),
|
|
244
|
+
}).default({}),
|
|
245
|
+
/**
|
|
246
|
+
* whether enable IE automatically download open
|
|
247
|
+
*/
|
|
248
|
+
noopen: z.object({
|
|
249
|
+
match: IgnoreOrMatchOption,
|
|
250
|
+
ignore: IgnoreOrMatchOption,
|
|
251
|
+
/**
|
|
252
|
+
* Default to `true`
|
|
253
|
+
*/
|
|
254
|
+
enable: z.boolean().default(true),
|
|
255
|
+
}).default({}),
|
|
256
|
+
/**
|
|
257
|
+
* whether enable IE8 automatically detect mime
|
|
258
|
+
*/
|
|
259
|
+
nosniff: z.object({
|
|
260
|
+
match: IgnoreOrMatchOption,
|
|
261
|
+
ignore: IgnoreOrMatchOption,
|
|
262
|
+
/**
|
|
263
|
+
* Default to `true`
|
|
264
|
+
*/
|
|
265
|
+
enable: z.boolean().default(true),
|
|
266
|
+
}).default({}),
|
|
267
|
+
/**
|
|
268
|
+
* whether enable IE8 XSS Filter
|
|
269
|
+
*/
|
|
270
|
+
xssProtection: z.object({
|
|
271
|
+
match: IgnoreOrMatchOption,
|
|
272
|
+
ignore: IgnoreOrMatchOption,
|
|
273
|
+
/**
|
|
274
|
+
* Default to `true`
|
|
275
|
+
*/
|
|
276
|
+
enable: z.boolean().default(true),
|
|
277
|
+
/**
|
|
278
|
+
* X-XSS-Protection response header value
|
|
279
|
+
*
|
|
280
|
+
* Default to `'1; mode=block'`
|
|
281
|
+
*/
|
|
282
|
+
value: z.coerce.string().default('1; mode=block'),
|
|
283
|
+
}).default({}),
|
|
284
|
+
/**
|
|
285
|
+
* content security policy config
|
|
286
|
+
*/
|
|
287
|
+
csp: z.object({
|
|
288
|
+
match: IgnoreOrMatchOption,
|
|
289
|
+
ignore: IgnoreOrMatchOption,
|
|
290
|
+
/**
|
|
291
|
+
* Default to `false`
|
|
292
|
+
*/
|
|
293
|
+
enable: z.boolean().default(false),
|
|
294
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#csp_overview
|
|
295
|
+
policy: z.record(z.union([ z.string(), z.array(z.string()), z.boolean() ])).default({}),
|
|
296
|
+
/**
|
|
297
|
+
* whether enable report only mode
|
|
298
|
+
* Default to `undefined`
|
|
299
|
+
*/
|
|
300
|
+
reportOnly: z.boolean().optional(),
|
|
301
|
+
/**
|
|
302
|
+
* whether support IE
|
|
303
|
+
* Default to `undefined`
|
|
304
|
+
*/
|
|
305
|
+
supportIE: z.boolean().optional(),
|
|
306
|
+
}).default({}),
|
|
307
|
+
/**
|
|
308
|
+
* whether enable referrer policy
|
|
309
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
|
|
310
|
+
*/
|
|
311
|
+
referrerPolicy: z.object({
|
|
312
|
+
match: IgnoreOrMatchOption,
|
|
313
|
+
ignore: IgnoreOrMatchOption,
|
|
314
|
+
/**
|
|
315
|
+
* Default to `false`
|
|
316
|
+
*/
|
|
317
|
+
enable: z.boolean().default(false),
|
|
318
|
+
/**
|
|
319
|
+
* referrer policy value
|
|
320
|
+
*
|
|
321
|
+
* Default to `'no-referrer-when-downgrade'`
|
|
322
|
+
*/
|
|
323
|
+
value: z.string().default('no-referrer-when-downgrade'),
|
|
324
|
+
}).default({}),
|
|
325
|
+
/**
|
|
326
|
+
* whether enable auto avoid directory traversal attack
|
|
327
|
+
*/
|
|
328
|
+
dta: z.object({
|
|
329
|
+
match: IgnoreOrMatchOption,
|
|
330
|
+
ignore: IgnoreOrMatchOption,
|
|
331
|
+
/**
|
|
332
|
+
* Default to `true`
|
|
333
|
+
*/
|
|
334
|
+
enable: z.boolean().default(true),
|
|
335
|
+
}).default({}),
|
|
336
|
+
ssrf: z.object({
|
|
337
|
+
ipBlackList: z.array(z.string()).optional(),
|
|
338
|
+
ipExceptionList: z.array(z.string()).optional(),
|
|
339
|
+
hostnameExceptionList: z.array(z.string()).optional(),
|
|
340
|
+
checkAddress: SSRFCheckAddressFunction.optional(),
|
|
341
|
+
}).default({}),
|
|
342
|
+
match: z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional(),
|
|
343
|
+
ignore: z.union([ IgnoreOrMatch, IgnoreOrMatch.array() ]).optional(),
|
|
344
|
+
__protocolWhiteListSet: z.set(z.string()).optional().readonly(),
|
|
345
|
+
});
|
|
346
|
+
export type SecurityConfig = z.infer<typeof SecurityConfig>;
|
|
347
|
+
|
|
348
|
+
const SecurityHelperOnTagAttrHandler = z.function()
|
|
349
|
+
.args(z.string(), z.string(), z.string(), z.boolean())
|
|
350
|
+
.returns(z.union([ z.string(), z.void() ]));
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* (tag: string, name: string, value: string, isWhiteAttr: boolean) => string | void
|
|
354
|
+
*/
|
|
355
|
+
export type SecurityHelperOnTagAttrHandler = z.infer<typeof SecurityHelperOnTagAttrHandler>;
|
|
356
|
+
|
|
357
|
+
export const SecurityHelperConfig = z.object({
|
|
358
|
+
shtml: z.object({
|
|
359
|
+
/**
|
|
360
|
+
* tag attribute white list
|
|
361
|
+
*/
|
|
362
|
+
whiteList: z.record(z.array(z.string())).optional(),
|
|
363
|
+
/**
|
|
364
|
+
* domain white list
|
|
365
|
+
* @deprecated use `config.security.domainWhiteList` instead
|
|
366
|
+
*/
|
|
367
|
+
domainWhiteList: z.array(z.string()).optional(),
|
|
368
|
+
/**
|
|
369
|
+
* tag attribute handler
|
|
370
|
+
*/
|
|
371
|
+
onTagAttr: SecurityHelperOnTagAttrHandler.optional(),
|
|
372
|
+
}).default({}),
|
|
373
|
+
});
|
|
374
|
+
export type SecurityHelperConfig = z.infer<typeof SecurityHelperConfig>;
|
|
375
|
+
|
|
376
|
+
export default {
|
|
377
|
+
security: SecurityConfig.parse({}),
|
|
378
|
+
helper: SecurityHelperConfig.parse({}),
|
|
379
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import './types.js';
|
|
2
|
+
|
|
3
|
+
// module.exports = require('./app/middleware/securities');
|
|
4
|
+
// module.exports.csp = require('./lib/middlewares/csp');
|
|
5
|
+
// module.exports.csrf = require('./lib/middlewares/csrf');
|
|
6
|
+
// module.exports.methodNoAllow = require('./lib/middlewares/methodnoallow');
|
|
7
|
+
// module.exports.noopen = require('./lib/middlewares/noopen');
|
|
8
|
+
// module.exports.nosniff = require('./lib/middlewares/nosniff');
|
|
9
|
+
// module.exports.xssProtection = require('./lib/middlewares/xssProtection');
|
|
10
|
+
// module.exports.xframe = require('./lib/middlewares/xframe');
|
|
11
|
+
// module.exports.safeRedirect = require('./lib/safe_redirect');
|
|
12
|
+
// module.exports.utils = require('./lib/utils');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EggCore } from '@eggjs/core';
|
|
2
|
+
import type { SSRFCheckAddressFunction } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
const SSRF_HTTPCLIENT = Symbol('SSRF_HTTPCLIENT');
|
|
5
|
+
|
|
6
|
+
type HttpClient = EggCore['HttpClient'];
|
|
7
|
+
type HttpClientParameters = Parameters<HttpClient['prototype']['request']>;
|
|
8
|
+
export type HttpClientRequestURL = HttpClientParameters[0];
|
|
9
|
+
export type HttpClientOptions = HttpClientParameters[1] & { checkAddress?: SSRFCheckAddressFunction };
|
|
10
|
+
export type HttpClientResponse<T = any> = Awaited<ReturnType<HttpClient['prototype']['request']>> & { data: T };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* safe curl with ssrf protection
|
|
14
|
+
*/
|
|
15
|
+
export async function safeCurlForApplication<T = any>(app: EggCore, url: HttpClientRequestURL, options: HttpClientOptions = {}) {
|
|
16
|
+
const ssrfConfig = app.config.security.ssrf;
|
|
17
|
+
if (ssrfConfig?.checkAddress) {
|
|
18
|
+
options.checkAddress = ssrfConfig.checkAddress;
|
|
19
|
+
} else {
|
|
20
|
+
app.logger.warn('[@eggjs/security] please configure `config.security.ssrf` first');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (ssrfConfig?.checkAddress) {
|
|
24
|
+
let httpClient = app[SSRF_HTTPCLIENT] as ReturnType<EggCore['createHttpClient']>;
|
|
25
|
+
// use the new httpClient init with checkAddress
|
|
26
|
+
if (!httpClient) {
|
|
27
|
+
httpClient = app[SSRF_HTTPCLIENT] = app.createHttpClient({
|
|
28
|
+
checkAddress: ssrfConfig.checkAddress,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return await httpClient.request<T>(url, options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return await app.curl<T>(url, options);
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remote command execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BASIC_ALPHABETS = new Set('abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.-_'.split(''));
|
|
6
|
+
|
|
7
|
+
export default function cliFilter(text: string) {
|
|
8
|
+
const str = '' + text;
|
|
9
|
+
let res = '';
|
|
10
|
+
let ascii;
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < str.length; index++) {
|
|
13
|
+
ascii = str[index];
|
|
14
|
+
if (BASIC_ALPHABETS.has(ascii)) {
|
|
15
|
+
res += ascii;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return res;
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const BASIC_ALPHABETS = new Set('#&;`|*?~<>^()[]{}$;\'",\x0A\xFF'.split(''));
|
|
2
|
+
|
|
3
|
+
export default function escapeShellCmd(text: string) {
|
|
4
|
+
const str = '' + text;
|
|
5
|
+
let res = '';
|
|
6
|
+
let ascii;
|
|
7
|
+
|
|
8
|
+
for (let index = 0; index < str.length; index++) {
|
|
9
|
+
ascii = str[index];
|
|
10
|
+
if (!BASIC_ALPHABETS.has(ascii)) {
|
|
11
|
+
res += ascii;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return res;
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import cliFilter from './cliFilter.js';
|
|
2
|
+
import escape from './escape.js';
|
|
3
|
+
import escapeShellArg from './escapeShellArg.js';
|
|
4
|
+
import escapeShellCmd from './escapeShellCmd.js';
|
|
5
|
+
import shtml from './shtml.js';
|
|
6
|
+
import sjs from './sjs.js';
|
|
7
|
+
import sjson from './sjson.js';
|
|
8
|
+
import spath from './spath.js';
|
|
9
|
+
import surl from './surl.js';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
cliFilter,
|
|
13
|
+
escape,
|
|
14
|
+
escapeShellArg,
|
|
15
|
+
escapeShellCmd,
|
|
16
|
+
shtml,
|
|
17
|
+
sjs,
|
|
18
|
+
sjson,
|
|
19
|
+
spath,
|
|
20
|
+
surl,
|
|
21
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BaseContextClass } from '@eggjs/core';
|
|
2
|
+
import xss from 'xss';
|
|
3
|
+
import { isSafeDomain, getFromUrl } from '../utils.js';
|
|
4
|
+
import type { SecurityHelperOnTagAttrHandler } from '../../types.js';
|
|
5
|
+
|
|
6
|
+
const BUILD_IN_ON_TAG_ATTR = Symbol('buildInOnTagAttr');
|
|
7
|
+
|
|
8
|
+
// default rule: https://github.com/leizongmin/js-xss/blob/master/lib/default.js
|
|
9
|
+
// add domain filter based on xss module
|
|
10
|
+
// custom options http://jsxss.com/zh/options.html
|
|
11
|
+
// eg: support a tag,filter attributes except for title : whiteList: {a: ['title']}
|
|
12
|
+
export default function shtml(this: BaseContextClass, val: string) {
|
|
13
|
+
if (typeof val !== 'string') {
|
|
14
|
+
return val;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const securityOptions = this.ctx.securityOptions;
|
|
18
|
+
let buildInOnTagAttrHandler: SecurityHelperOnTagAttrHandler | undefined;
|
|
19
|
+
const shtmlConfig = {
|
|
20
|
+
...this.app.config.helper.shtml,
|
|
21
|
+
...securityOptions.shtml,
|
|
22
|
+
[BUILD_IN_ON_TAG_ATTR]: buildInOnTagAttrHandler,
|
|
23
|
+
};
|
|
24
|
+
const domainWhiteList = this.app.config.security.domainWhiteList;
|
|
25
|
+
const app = this.app;
|
|
26
|
+
// filter href and src attribute if not in domain white list
|
|
27
|
+
if (!shtmlConfig[BUILD_IN_ON_TAG_ATTR]) {
|
|
28
|
+
shtmlConfig[BUILD_IN_ON_TAG_ATTR] = (_tag, name, value, isWhiteAttr) => {
|
|
29
|
+
if (isWhiteAttr && (name === 'href' || name === 'src')) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
value = String(value);
|
|
35
|
+
if (value[0] === '/' || value[0] === '#') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hostname = getFromUrl(value, 'hostname');
|
|
40
|
+
if (!hostname) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If we don't have our hostname in the app.security.domainWhiteList,
|
|
45
|
+
// Just check for `shtmlConfig.domainWhiteList` and `ctx.whiteList`.
|
|
46
|
+
if (!isSafeDomain(hostname, domainWhiteList)) {
|
|
47
|
+
// Check for `shtmlConfig.domainWhiteList` first (duplicated now)
|
|
48
|
+
if (shtmlConfig.domainWhiteList && shtmlConfig.domainWhiteList.length > 0) {
|
|
49
|
+
app.deprecate('[@eggjs/security/lib/helper/shtml] `config.helper.shtml.domainWhiteList` has been deprecate. Please use `config.security.domainWhiteList` instead.');
|
|
50
|
+
if (!isSafeDomain(hostname, shtmlConfig.domainWhiteList)) {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// avoid overriding user configuration 'onTagAttr'
|
|
61
|
+
if (shtmlConfig.onTagAttr) {
|
|
62
|
+
const customOnTagAttrHandler = shtmlConfig.onTagAttr;
|
|
63
|
+
shtmlConfig.onTagAttr = function(tag, name, value, isWhiteAttr) {
|
|
64
|
+
const result = customOnTagAttrHandler.apply(this, [ tag, name, value, isWhiteAttr ]);
|
|
65
|
+
if (result !== undefined) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
// fallback to build-in handler
|
|
69
|
+
return shtmlConfig[BUILD_IN_ON_TAG_ATTR]!.apply(this, [ tag, name, value, isWhiteAttr ]);
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
shtmlConfig.onTagAttr = shtmlConfig[BUILD_IN_ON_TAG_ATTR];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return xss(val, shtmlConfig);
|
|
77
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape JavaScript to \xHH format
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// escape \x00-\x7f
|
|
6
|
+
// except 0-9,A-Z,a-z(\x2f-\x3a \x40-\x5b \x60-\x7b)
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line
|
|
9
|
+
const MATCH_VULNERABLE_REGEXP = /[\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]/;
|
|
10
|
+
// eslint-enable-next-line
|
|
11
|
+
|
|
12
|
+
const BASIC_ALPHABETS = new Set('abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''));
|
|
13
|
+
|
|
14
|
+
const map: Record<string, string> = {
|
|
15
|
+
'\t': '\\t',
|
|
16
|
+
'\n': '\\n',
|
|
17
|
+
'\r': '\\r',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function escapeJavaScript(text: string) {
|
|
21
|
+
const str = '' + text;
|
|
22
|
+
const match = MATCH_VULNERABLE_REGEXP.exec(str);
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
return str;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let res = '';
|
|
29
|
+
let index = 0;
|
|
30
|
+
let lastIndex = 0;
|
|
31
|
+
let ascii;
|
|
32
|
+
|
|
33
|
+
for (index = match.index; index < str.length; index++) {
|
|
34
|
+
ascii = str[index];
|
|
35
|
+
if (BASIC_ALPHABETS.has(ascii)) {
|
|
36
|
+
continue;
|
|
37
|
+
} else {
|
|
38
|
+
if (map[ascii] === undefined) {
|
|
39
|
+
const code = ascii.charCodeAt(0);
|
|
40
|
+
if (code > 127) {
|
|
41
|
+
continue;
|
|
42
|
+
} else {
|
|
43
|
+
map[ascii] = '\\x' + code.toString(16);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (lastIndex !== index) {
|
|
49
|
+
res += str.substring(lastIndex, index);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lastIndex = index + 1;
|
|
53
|
+
res += map[ascii];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lastIndex !== index ? res + str.substring(lastIndex, index) : res;
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import sjs from './sjs.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* escape json
|
|
5
|
+
* for output json in script
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function sanitizeKey(obj: any) {
|
|
9
|
+
if (typeof obj !== 'object') return obj;
|
|
10
|
+
if (Array.isArray(obj)) return obj;
|
|
11
|
+
if (obj === null) return null;
|
|
12
|
+
if (typeof obj === 'boolean') return obj;
|
|
13
|
+
if (typeof obj === 'number') return obj;
|
|
14
|
+
if (Buffer.isBuffer(obj)) return obj.toString();
|
|
15
|
+
|
|
16
|
+
for (const k in obj) {
|
|
17
|
+
const escapedK = sjs(k);
|
|
18
|
+
if (escapedK !== k) {
|
|
19
|
+
obj[escapedK] = sanitizeKey(obj[k]);
|
|
20
|
+
obj[k] = undefined;
|
|
21
|
+
} else {
|
|
22
|
+
obj[k] = sanitizeKey(obj[k]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function jsonEscape(obj: any) {
|
|
29
|
+
return JSON.stringify(sanitizeKey(obj), (_k, v) => {
|
|
30
|
+
if (typeof v === 'string') {
|
|
31
|
+
return sjs(v);
|
|
32
|
+
}
|
|
33
|
+
return v;
|
|
34
|
+
});
|
|
35
|
+
}
|