├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ ├── pkg.pr.new.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── __snapshots__ ├── config.default.test.ts.js ├── context.test.ts.js ├── csp.test.ts.js ├── csrf.test.ts.js ├── dta.test.ts.js └── xss.test.ts.js ├── package.json ├── src ├── agent.ts ├── app.ts ├── app │ ├── extend │ │ ├── agent.ts │ │ ├── application.ts │ │ ├── context.ts │ │ ├── helper.ts │ │ └── response.ts │ └── middleware │ │ └── securities.ts ├── config │ ├── config.default.ts │ └── config.local.ts ├── index.ts ├── lib │ ├── extend │ │ └── safe_curl.ts │ ├── helper │ │ ├── cliFilter.ts │ │ ├── escape.ts │ │ ├── escapeShellArg.ts │ │ ├── escapeShellCmd.ts │ │ ├── index.ts │ │ ├── shtml.ts │ │ ├── sjs.ts │ │ ├── sjson.ts │ │ ├── spath.ts │ │ └── surl.ts │ ├── middlewares │ │ ├── csp.ts │ │ ├── csrf.ts │ │ ├── dta.ts │ │ ├── hsts.ts │ │ ├── index.ts │ │ ├── methodnoallow.ts │ │ ├── noopen.ts │ │ ├── nosniff.ts │ │ ├── referrerPolicy.ts │ │ ├── xframe.ts │ │ └── xssProtection.ts │ └── utils.ts ├── types.ts └── typings │ └── index.d.ts ├── test ├── app │ └── extends │ │ ├── cliFilter.test.ts │ │ ├── escapeShellArg.test.ts │ │ ├── escapeShellCmd.test.ts │ │ ├── helper.test.ts │ │ ├── sjs.test.ts │ │ ├── sjson.test.ts │ │ └── spath.test.ts ├── benchmark.js ├── benchmark │ ├── cidr_subnet.js │ └── set_header.js ├── config │ └── config.default.test.ts ├── context.test.ts ├── csp.test.ts ├── csrf.test.ts ├── csrf_cookieDomain.test.ts ├── dta.test.ts ├── fixtures │ └── apps │ │ ├── csp-ignore │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csp-reportonly │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csp-supportie │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csp │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-cookieOptions-signed │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── csrf-cookieOptions │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── csrf-empty-referer │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-enable-false │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-error-type │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-ignorejson │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── csrf-multiple │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-session-disable │ │ ├── config │ │ │ ├── config.default.js │ │ │ └── plugin.js │ │ └── package.json │ │ ├── csrf-string-cookiedomain │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── csrf-supported-override-default │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf-supported-requests-default-config │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── csrf-supported-requests │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── csrf │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── ctoken │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── dta │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-app-surlextend │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-app │ │ ├── app.js │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-cliFilter-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-config-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ ├── helper-app │ │ │ ├── app │ │ │ │ └── router.js │ │ │ └── package.json │ │ └── package.json │ │ ├── helper-escapeShellArg-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-escapeShellCmd-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-link-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-sjs-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-sjson-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── helper-spath-app │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── hsts-default │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── hsts-nosub │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── hsts │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── iframe-allowfrom │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── iframe-black-urls │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── iframe-novalue │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── iframe │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── inject │ │ ├── app │ │ │ ├── router.js │ │ │ └── view │ │ │ │ └── index.nj │ │ ├── config │ │ │ ├── config.js │ │ │ └── plugin.js │ │ └── package.json │ │ ├── isSafeDomain-custom │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── isSafeDomain │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── method │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── noopen │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── nosniff │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── referrer-config-compatibility │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── referrer-config │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── referrer │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── safe_redirect │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── safe_redirect_noconfig │ │ ├── app │ │ │ ├── controller │ │ │ │ └── home.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── security-override-controller │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── security-override-middleware │ │ ├── app │ │ │ ├── middleware │ │ │ │ └── override.js │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── security-unset │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── security │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── ssrf-check-address-useHttpClientNext │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── ssrf-check-address │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── ssrf-hostname-exception-list │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── ssrf-ip-black-list │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── ssrf-ip-exception-list │ │ ├── config │ │ │ └── config.default.js │ │ └── package.json │ │ ├── utils-check-if-pass │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── utils-check-if-pass2 │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── utils-check-if-pass3 │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── utils-check-if-pass4 │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── utils-check-if-pass5 │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── utils-check-if-pass6 │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── xss-close-zero │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ ├── xss-close │ │ ├── app │ │ │ └── router.js │ │ ├── config │ │ │ └── config.js │ │ └── package.json │ │ └── xss │ │ ├── app │ │ └── router.js │ │ ├── config │ │ └── config.js │ │ └── package.json ├── hsts.test.ts ├── inject.test.ts ├── lib │ └── helper │ │ └── surl.test.ts ├── method_not_allow.test.ts ├── noopen.test.ts ├── nosniff.test.ts ├── referrer.test.ts ├── safe_redirect.test.ts ├── security.test.ts ├── ssrf.test.ts ├── utils.test.ts ├── xframe.test.ts └── xss.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | __snapshots__ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | version: '18.19.0, 20, 22' 15 | secrets: 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - run: corepack enable 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Build 21 | run: npm run prepublishOnly --if-present 22 | 23 | - run: npx pkg-pr-new publish 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: eggjs/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | test/fixtures/**/run 6 | .DS_Store 7 | .tshy* 8 | .eslintcache 9 | dist 10 | package-lock.json 11 | .package-lock.json 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /__snapshots__/config.default.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/config/config.default.test.ts should config default values keep stable 1'] = { 2 | "security": { 3 | "domainWhiteList": [], 4 | "protocolWhiteList": [], 5 | "defaultMiddleware": [ 6 | "csrf", 7 | "hsts", 8 | "methodnoallow", 9 | "noopen", 10 | "nosniff", 11 | "csp", 12 | "xssProtection", 13 | "xframe", 14 | "dta" 15 | ], 16 | "csrf": { 17 | "enable": true, 18 | "type": "ctoken", 19 | "ignoreJSON": false, 20 | "cookieName": "csrfToken", 21 | "sessionName": "csrfToken", 22 | "headerName": "x-csrf-token", 23 | "bodyName": "_csrf", 24 | "queryName": "_csrf", 25 | "rotateWhenInvalid": false, 26 | "useSession": false, 27 | "supportedRequests": [ 28 | { 29 | "path": {}, 30 | "methods": [ 31 | "POST", 32 | "PATCH", 33 | "DELETE", 34 | "PUT", 35 | "CONNECT" 36 | ] 37 | } 38 | ], 39 | "refererWhiteList": [], 40 | "cookieOptions": { 41 | "signed": false, 42 | "httpOnly": false, 43 | "overwrite": true 44 | } 45 | }, 46 | "xframe": { 47 | "enable": true, 48 | "value": "SAMEORIGIN" 49 | }, 50 | "hsts": { 51 | "enable": false, 52 | "maxAge": 31536000, 53 | "includeSubdomains": false 54 | }, 55 | "methodnoallow": { 56 | "enable": true 57 | }, 58 | "noopen": { 59 | "enable": true 60 | }, 61 | "nosniff": { 62 | "enable": true 63 | }, 64 | "xssProtection": { 65 | "enable": true, 66 | "value": "1; mode=block" 67 | }, 68 | "csp": { 69 | "enable": false, 70 | "policy": {} 71 | }, 72 | "referrerPolicy": { 73 | "enable": false, 74 | "value": "no-referrer-when-downgrade" 75 | }, 76 | "dta": { 77 | "enable": true 78 | }, 79 | "ssrf": {} 80 | }, 81 | "helper": { 82 | "shtml": {} 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /__snapshots__/context.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/context.test.ts context.isSafeDomain should return false when domains are not safe 1'] = { 2 | "domainWhiteList": [ 3 | ".domain.com", 4 | "http://www.baidu.com", 5 | "192.*.0.*", 6 | "*.alibaba.com" 7 | ], 8 | "protocolWhiteList": [], 9 | "defaultMiddleware": "xframe", 10 | "csrf": { 11 | "enable": true, 12 | "type": "ctoken", 13 | "ignoreJSON": false, 14 | "cookieName": "csrfToken", 15 | "sessionName": "csrfToken", 16 | "headerName": "x-csrf-token", 17 | "bodyName": "_csrf", 18 | "queryName": "_csrf", 19 | "rotateWhenInvalid": false, 20 | "useSession": false, 21 | "supportedRequests": [ 22 | { 23 | "path": {}, 24 | "methods": [ 25 | "POST", 26 | "PATCH", 27 | "DELETE", 28 | "PUT", 29 | "CONNECT" 30 | ] 31 | } 32 | ], 33 | "refererWhiteList": [], 34 | "cookieOptions": { 35 | "signed": false, 36 | "httpOnly": false, 37 | "overwrite": true 38 | } 39 | }, 40 | "xframe": { 41 | "enable": true, 42 | "value": "SAMEORIGIN" 43 | }, 44 | "hsts": { 45 | "enable": false, 46 | "maxAge": 31536000, 47 | "includeSubdomains": false 48 | }, 49 | "methodnoallow": { 50 | "enable": true 51 | }, 52 | "noopen": { 53 | "enable": true 54 | }, 55 | "nosniff": { 56 | "enable": true 57 | }, 58 | "xssProtection": { 59 | "enable": true, 60 | "value": "1; mode=block" 61 | }, 62 | "csp": { 63 | "enable": false, 64 | "policy": {} 65 | }, 66 | "referrerPolicy": { 67 | "enable": false, 68 | "value": "no-referrer-when-downgrade" 69 | }, 70 | "dta": { 71 | "enable": true 72 | }, 73 | "ssrf": {} 74 | } 75 | -------------------------------------------------------------------------------- /__snapshots__/csp.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/csp.test.ts should ignore path 1'] = { 2 | "domainWhiteList": [], 3 | "protocolWhiteList": [], 4 | "defaultMiddleware": "csp", 5 | "csrf": { 6 | "enable": true, 7 | "type": "ctoken", 8 | "ignoreJSON": false, 9 | "cookieName": "csrfToken", 10 | "sessionName": "csrfToken", 11 | "headerName": "x-csrf-token", 12 | "bodyName": "_csrf", 13 | "queryName": "_csrf", 14 | "rotateWhenInvalid": false, 15 | "useSession": false, 16 | "supportedRequests": [ 17 | { 18 | "path": {}, 19 | "methods": [ 20 | "POST", 21 | "PATCH", 22 | "DELETE", 23 | "PUT", 24 | "CONNECT" 25 | ] 26 | } 27 | ], 28 | "refererWhiteList": [], 29 | "cookieOptions": { 30 | "signed": false, 31 | "httpOnly": false, 32 | "overwrite": true 33 | } 34 | }, 35 | "xframe": { 36 | "enable": true, 37 | "value": "SAMEORIGIN" 38 | }, 39 | "hsts": { 40 | "enable": false, 41 | "maxAge": 31536000, 42 | "includeSubdomains": false 43 | }, 44 | "methodnoallow": { 45 | "enable": true 46 | }, 47 | "noopen": { 48 | "enable": true 49 | }, 50 | "nosniff": { 51 | "enable": true 52 | }, 53 | "xssProtection": { 54 | "enable": true, 55 | "value": "1; mode=block" 56 | }, 57 | "csp": { 58 | "enable": true, 59 | "policy": { 60 | "script-src": [ 61 | "'self'", 62 | "'unsafe-inline'", 63 | "'unsafe-eval'", 64 | "www.google-analytics.com" 65 | ], 66 | "style-src": [ 67 | "'unsafe-inline'", 68 | "www.google-analytics.com" 69 | ], 70 | "img-src": [ 71 | "'self'", 72 | "data:", 73 | "www.google-analytics.com" 74 | ], 75 | "frame-ancestors": [ 76 | "'self'" 77 | ], 78 | "report-uri": "http://pointman.domain.com/csp?app=csp" 79 | }, 80 | "ignore": [ 81 | "/api/", 82 | {} 83 | ] 84 | }, 85 | "referrerPolicy": { 86 | "enable": false, 87 | "value": "no-referrer-when-downgrade" 88 | }, 89 | "dta": { 90 | "enable": true 91 | }, 92 | "ssrf": {} 93 | } 94 | -------------------------------------------------------------------------------- /__snapshots__/csrf.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/csrf.test.ts should update form with csrf token 1'] = { 2 | "enable": true, 3 | "type": "ctoken", 4 | "ignoreJSON": false, 5 | "cookieName": "csrfToken", 6 | "sessionName": "csrfToken", 7 | "headerName": "x-csrf-token", 8 | "bodyName": "_csrf", 9 | "queryName": "_csrf", 10 | "rotateWhenInvalid": false, 11 | "useSession": false, 12 | "supportedRequests": [ 13 | { 14 | "path": {}, 15 | "methods": [ 16 | "POST", 17 | "PATCH", 18 | "DELETE", 19 | "PUT", 20 | "CONNECT" 21 | ] 22 | } 23 | ], 24 | "refererWhiteList": [], 25 | "cookieOptions": { 26 | "signed": false, 27 | "httpOnly": false, 28 | "overwrite": true 29 | }, 30 | "ignore": [ 31 | {}, 32 | null 33 | ] 34 | } 35 | 36 | exports['test/csrf.test.ts apps/csrf-supported-requests-default-config should works without error because csrf = false override default config 1'] = { 37 | "enable": false, 38 | "type": "ctoken", 39 | "ignoreJSON": false, 40 | "cookieName": "csrfToken", 41 | "sessionName": "csrfToken", 42 | "headerName": "x-csrf-token", 43 | "bodyName": "_csrf", 44 | "queryName": "_csrf", 45 | "rotateWhenInvalid": false, 46 | "useSession": false, 47 | "supportedRequests": [ 48 | { 49 | "path": {}, 50 | "methods": [ 51 | "POST", 52 | "PATCH", 53 | "DELETE", 54 | "PUT", 55 | "CONNECT" 56 | ] 57 | } 58 | ], 59 | "refererWhiteList": [], 60 | "cookieOptions": { 61 | "signed": false, 62 | "httpOnly": false, 63 | "overwrite": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /__snapshots__/dta.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/dta.test.ts should ok when path is normal 1'] = { 2 | "domainWhiteList": [], 3 | "protocolWhiteList": [], 4 | "defaultMiddleware": "dta", 5 | "csrf": { 6 | "enable": true, 7 | "type": "ctoken", 8 | "ignoreJSON": false, 9 | "cookieName": "csrfToken", 10 | "sessionName": "csrfToken", 11 | "headerName": "x-csrf-token", 12 | "bodyName": "_csrf", 13 | "queryName": "_csrf", 14 | "rotateWhenInvalid": false, 15 | "useSession": false, 16 | "supportedRequests": [ 17 | { 18 | "path": {}, 19 | "methods": [ 20 | "POST", 21 | "PATCH", 22 | "DELETE", 23 | "PUT", 24 | "CONNECT" 25 | ] 26 | } 27 | ], 28 | "refererWhiteList": [], 29 | "cookieOptions": { 30 | "signed": false, 31 | "httpOnly": false, 32 | "overwrite": true 33 | } 34 | }, 35 | "xframe": { 36 | "enable": true, 37 | "value": "SAMEORIGIN" 38 | }, 39 | "hsts": { 40 | "enable": false, 41 | "maxAge": 31536000, 42 | "includeSubdomains": false 43 | }, 44 | "methodnoallow": { 45 | "enable": true 46 | }, 47 | "noopen": { 48 | "enable": true 49 | }, 50 | "nosniff": { 51 | "enable": true 52 | }, 53 | "xssProtection": { 54 | "enable": true, 55 | "value": "1; mode=block" 56 | }, 57 | "csp": { 58 | "enable": false, 59 | "policy": {} 60 | }, 61 | "referrerPolicy": { 62 | "enable": false, 63 | "value": "no-referrer-when-downgrade" 64 | }, 65 | "dta": { 66 | "enable": true 67 | }, 68 | "ssrf": {} 69 | } 70 | -------------------------------------------------------------------------------- /__snapshots__/xss.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/xss.test.ts should set X-XSS-Protection header value 0 when config is number 0 1'] = { 2 | "enable": true, 3 | "value": 0 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eggjs/security", 3 | "version": "4.0.1", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "security plugin in egg framework", 8 | "eggPlugin": { 9 | "name": "security", 10 | "optionalDependencies": [ 11 | "session" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm", 15 | "require": "./dist/commonjs", 16 | "typescript": "./src" 17 | } 18 | }, 19 | "keywords": [ 20 | "egg", 21 | "eggPlugin", 22 | "egg-plugin", 23 | "security" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/eggjs/security.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/eggjs/egg/issues" 31 | }, 32 | "homepage": "https://github.com/eggjs/security#readme", 33 | "author": "jtyjty99999", 34 | "license": "MIT", 35 | "engines": { 36 | "node": ">= 18.19.0" 37 | }, 38 | "dependencies": { 39 | "@eggjs/core": "^6.2.13", 40 | "@eggjs/ip": "^2.1.0", 41 | "csrf": "^3.0.6", 42 | "egg-path-matching": "^2.1.0", 43 | "escape-html": "^1.0.3", 44 | "extend": "^3.0.1", 45 | "koa-compose": "^4.1.0", 46 | "matcher": "^4.0.0", 47 | "nanoid": "^3.3.8", 48 | "type-is": "^1.6.18", 49 | "xss": "^1.0.3", 50 | "zod": "^3.24.1" 51 | }, 52 | "devDependencies": { 53 | "@arethetypeswrong/cli": "^0.17.1", 54 | "@eggjs/bin": "7", 55 | "@eggjs/mock": "^6.0.5", 56 | "@eggjs/supertest": "^8.2.0", 57 | "@eggjs/tsconfig": "1", 58 | "@types/escape-html": "^1.0.4", 59 | "@types/extend": "^3.0.4", 60 | "@types/koa-compose": "^3.2.8", 61 | "@types/mocha": "10", 62 | "@types/node": "22", 63 | "@types/type-is": "^1.6.7", 64 | "beautify-benchmark": "^0.2.4", 65 | "benchmark": "^2.1.4", 66 | "egg": "^4.0.4", 67 | "egg-view-nunjucks": "^2.3.0", 68 | "eslint": "8", 69 | "eslint-config-egg": "14", 70 | "rimraf": "6", 71 | "snap-shot-it": "^7.9.10", 72 | "spy": "^1.0.0", 73 | "supertest": "^6.3.3", 74 | "tshy": "3", 75 | "tshy-after": "1", 76 | "typescript": "5" 77 | }, 78 | "scripts": { 79 | "lint": "eslint --cache src test --ext .ts", 80 | "pretest": "npm run clean && npm run lint -- --fix", 81 | "test": "egg-bin test", 82 | "test:snapshot:update": "SNAPSHOT_UPDATE=1 egg-bin test", 83 | "preci": "npm run clean && npm run lint", 84 | "ci": "egg-bin cov", 85 | "postci": "npm run prepublishOnly && npm run clean", 86 | "clean": "rimraf dist", 87 | "prepublishOnly": "tshy && tshy-after && attw --pack" 88 | }, 89 | "type": "module", 90 | "tshy": { 91 | "exports": { 92 | ".": "./src/index.ts", 93 | "./package.json": "./package.json" 94 | } 95 | }, 96 | "exports": { 97 | ".": { 98 | "import": { 99 | "types": "./dist/esm/index.d.ts", 100 | "default": "./dist/esm/index.js" 101 | }, 102 | "require": { 103 | "types": "./dist/commonjs/index.d.ts", 104 | "default": "./dist/commonjs/index.js" 105 | } 106 | }, 107 | "./package.json": "./package.json" 108 | }, 109 | "files": [ 110 | "dist", 111 | "src" 112 | ], 113 | "types": "./dist/commonjs/index.d.ts", 114 | "main": "./dist/commonjs/index.js", 115 | "module": "./dist/esm/index.js" 116 | } 117 | -------------------------------------------------------------------------------- /src/agent.ts: -------------------------------------------------------------------------------- 1 | import type { ILifecycleBoot, EggCore } from '@eggjs/core'; 2 | import { preprocessConfig } from './lib/utils.js'; 3 | 4 | export default class AgentBoot implements ILifecycleBoot { 5 | private readonly agent; 6 | 7 | constructor(agent: EggCore) { 8 | this.agent = agent; 9 | } 10 | 11 | async configWillLoad() { 12 | preprocessConfig(this.agent.config.security); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import type { ILifecycleBoot, EggCore } from '@eggjs/core'; 2 | import { preprocessConfig } from './lib/utils.js'; 3 | import { SecurityConfig } from './config/config.default.js'; 4 | 5 | export default class AppBoot implements ILifecycleBoot { 6 | private readonly app; 7 | 8 | constructor(app: EggCore) { 9 | this.app = app; 10 | } 11 | 12 | configWillLoad() { 13 | const app = this.app; 14 | app.config.coreMiddleware.push('securities'); 15 | // parse config and check if config is legal 16 | const parsed = SecurityConfig.parse(app.config.security); 17 | if (typeof app.config.security.csrf === 'boolean') { 18 | // support old config: `config.security.csrf = false` 19 | app.config.security.csrf = parsed.csrf; 20 | } 21 | 22 | if (app.config.security.csrf.enable) { 23 | const { ignoreJSON } = app.config.security.csrf; 24 | if (ignoreJSON) { 25 | app.deprecate('[@eggjs/security/app] `config.security.csrf.ignoreJSON` is not safe now, please disable it.'); 26 | } 27 | } 28 | 29 | preprocessConfig(app.config.security); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/extend/agent.ts: -------------------------------------------------------------------------------- 1 | import { EggCore } from '@eggjs/core'; 2 | import { 3 | safeCurlForApplication, 4 | type HttpClientRequestURL, 5 | type HttpClientOptions, 6 | type HttpClientResponse, 7 | } from '../../lib/extend/safe_curl.js'; 8 | 9 | export default class SecurityAgent extends EggCore { 10 | async safeCurl( 11 | url: HttpClientRequestURL, options?: HttpClientOptions): Promise> { 12 | return await safeCurlForApplication(this, url, options); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/extend/application.ts: -------------------------------------------------------------------------------- 1 | import { EggCore } from '@eggjs/core'; 2 | import { 3 | safeCurlForApplication, 4 | type HttpClientRequestURL, 5 | type HttpClientOptions, 6 | type HttpClientResponse, 7 | } from '../../lib/extend/safe_curl.js'; 8 | 9 | const INPUT_CSRF = '\r\n'; 10 | const INJECTION_DEFENSE = ''; 11 | 12 | export default class SecurityApplication extends EggCore { 13 | injectCsrf(html: string) { 14 | html = html.replace(/()([\s\S]*?)<\/form>/gi, (_, $1, $2) => { 15 | const match = $2; 16 | if (match.indexOf('name="_csrf"') !== -1 || match.indexOf('name=\'_csrf\'') !== -1) { 17 | return $1 + match + ''; 18 | } 19 | return $1 + match + INPUT_CSRF; 20 | }); 21 | return html; 22 | } 23 | 24 | injectNonce(html: string) { 25 | html = html.replace(/([\s\S]*?)<\/script[^>]*?>/gi, (_, $1, $2) => { 26 | if (!$1.includes('nonce=')) { 27 | $1 += ' nonce="{{ctx.nonce}}"'; 28 | } 29 | return '' + $2 + ''; 30 | }); 31 | return html; 32 | } 33 | 34 | injectHijackingDefense(html: string) { 35 | return INJECTION_DEFENSE + html + INJECTION_DEFENSE; 36 | } 37 | 38 | async safeCurl( 39 | url: HttpClientRequestURL, options?: HttpClientOptions): Promise> { 40 | return await safeCurlForApplication(this, url, options); 41 | } 42 | } 43 | 44 | declare module '@eggjs/core' { 45 | interface EggCore { 46 | injectCsrf(html: string): string; 47 | injectNonce(html: string): string; 48 | injectHijackingDefense(html: string): string; 49 | safeCurl(url: HttpClientRequestURL, options?: HttpClientOptions): Promise>; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/extend/helper.ts: -------------------------------------------------------------------------------- 1 | import helpers from '../../lib/helper/index.js'; 2 | 3 | export default { 4 | ...helpers, 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/extend/response.ts: -------------------------------------------------------------------------------- 1 | import { Response as KoaResponse } from '@eggjs/core'; 2 | import SecurityContext from './context.js'; 3 | 4 | const unsafeRedirect = KoaResponse.prototype.redirect; 5 | 6 | export default class SecurityResponse extends KoaResponse { 7 | declare ctx: SecurityContext; 8 | 9 | /** 10 | * This is an unsafe redirection, and we WON'T check if the 11 | * destination url is safe or not. 12 | * Please DO NOT use this method unless in some very special cases, 13 | * otherwise there may be security vulnerabilities. 14 | * 15 | * @function Response#unsafeRedirect 16 | * @param {String} url URL to forward 17 | * @example 18 | * ```js 19 | * ctx.response.unsafeRedirect('http://www.domain.com'); 20 | * ctx.unsafeRedirect('http://www.domain.com'); 21 | * ``` 22 | */ 23 | unsafeRedirect(url: string, alt?: string) { 24 | unsafeRedirect.call(this, url, alt); 25 | } 26 | 27 | // app.response.unsafeRedirect = app.response.redirect; 28 | // delegate(app.context, 'response').method('unsafeRedirect'); 29 | /** 30 | * A safe redirection, and we'll check if the URL is in 31 | * a safe domain or not. 32 | * We've overridden the default Koa's implementation by adding a 33 | * white list as the filter for that. 34 | * 35 | * @function Response#redirect 36 | * @param {String} url URL to forward 37 | * @example 38 | * ```js 39 | * ctx.response.redirect('/login'); 40 | * ctx.redirect('/login'); 41 | * ``` 42 | */ 43 | redirect(url: string, alt?: string) { 44 | url = (url || '/').trim(); 45 | 46 | // Process with `//` 47 | if (url[0] === '/' && url[1] === '/') { 48 | url = '/'; 49 | } 50 | 51 | // if begin with '/', it means an internal jump 52 | if (url[0] === '/' && url[1] !== '\\') { 53 | this.unsafeRedirect(url, alt); 54 | return; 55 | } 56 | 57 | let urlObject: URL; 58 | try { 59 | urlObject = new URL(url); 60 | } catch { 61 | url = '/'; 62 | this.unsafeRedirect(url); 63 | return; 64 | } 65 | 66 | const domainWhiteList = this.app.config.security.domainWhiteList; 67 | if (urlObject.protocol !== 'http:' && urlObject.protocol !== 'https:') { 68 | url = '/'; 69 | } else if (!urlObject.hostname) { 70 | url = '/'; 71 | } else { 72 | if (domainWhiteList && domainWhiteList.length !== 0) { 73 | if (!this.ctx.isSafeDomain(urlObject.hostname)) { 74 | const message = `a security problem has been detected for url "${url}", redirection is prohibited.`; 75 | if (process.env.NODE_ENV === 'production') { 76 | this.app.coreLogger.warn('[@eggjs/security/response/redirect] %s', message); 77 | url = '/'; 78 | } else { 79 | // Exception will be thrown out in a non-PROD env. 80 | return this.ctx.throw(500, message); 81 | } 82 | } 83 | } 84 | } 85 | this.unsafeRedirect(url); 86 | } 87 | } 88 | 89 | declare module '@eggjs/core' { 90 | // add Response overrides types 91 | interface Response { 92 | unsafeRedirect(url: string, alt?: string): void; 93 | redirect(url: string, alt?: string): void; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/middleware/securities.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import compose from 'koa-compose'; 3 | import { pathMatching } from 'egg-path-matching'; 4 | import { EggCore, MiddlewareFunc } from '@eggjs/core'; 5 | import securityMiddlewares from '../../lib/middlewares/index.js'; 6 | import type { SecurityMiddlewareName } from '../../config/config.default.js'; 7 | 8 | export default (_: unknown, app: EggCore) => { 9 | const options = app.config.security; 10 | const middlewares: MiddlewareFunc[] = []; 11 | const defaultMiddlewares = typeof options.defaultMiddleware === 'string' 12 | ? options.defaultMiddleware.split(',').map(m => m.trim()).filter(m => !!m) as SecurityMiddlewareName[] 13 | : options.defaultMiddleware; 14 | 15 | if (options.match || options.ignore) { 16 | app.coreLogger.warn('[@eggjs/security/middleware/securities] Please set `match` or `ignore` on sub config'); 17 | } 18 | 19 | // format csrf.cookieDomain 20 | const originalCookieDomain = options.csrf.cookieDomain; 21 | if (originalCookieDomain && typeof originalCookieDomain !== 'function') { 22 | options.csrf.cookieDomain = () => originalCookieDomain; 23 | } 24 | 25 | defaultMiddlewares.forEach(middlewareName => { 26 | const opt = Reflect.get(options, middlewareName) as any; 27 | if (opt === false) { 28 | app.coreLogger.warn('[egg-security] Please use `config.security.%s = { enable: false }` instead of `config.security.%s = false`', middlewareName, middlewareName); 29 | } 30 | 31 | assert(opt === false || typeof opt === 'object', 32 | `config.security.${middlewareName} must be an object, or false(if you turn it off)`); 33 | 34 | if (opt === false || opt && opt.enable === false) { 35 | return; 36 | } 37 | 38 | if (middlewareName === 'csrf' && opt.useSession && !app.plugins.session) { 39 | throw new Error('csrf.useSession enabled, but session plugin is disabled'); 40 | } 41 | 42 | // use opt.match first (compatibility) 43 | if (opt.match && opt.ignore) { 44 | app.coreLogger.warn('[@eggjs/security/middleware/securities] `options.match` and `options.ignore` are both set, using `options.match`'); 45 | opt.ignore = undefined; 46 | } 47 | if (!opt.ignore && opt.blackUrls) { 48 | app.deprecate('[@eggjs/security/middleware/securities] Please use `config.security.xframe.ignore` instead, `config.security.xframe.blackUrls` will be removed very soon'); 49 | opt.ignore = opt.blackUrls; 50 | } 51 | // set matching function to security middleware options 52 | opt.matching = pathMatching(opt); 53 | 54 | const createMiddleware = securityMiddlewares[middlewareName]; 55 | const fn = createMiddleware(opt); 56 | middlewares.push(fn); 57 | app.coreLogger.info('[@eggjs/security/middleware/securities] use %s middleware', middlewareName); 58 | }); 59 | 60 | app.coreLogger.info('[@eggjs/security/middleware/securities] compose %d middlewares into one security middleware', 61 | middlewares.length); 62 | return compose(middlewares); 63 | }; 64 | -------------------------------------------------------------------------------- /src/config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { SecurityConfig } from '../types.js'; 2 | 3 | export default { 4 | security: { 5 | hsts: { 6 | enable: false, 7 | }, 8 | } as SecurityConfig, 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './types.js'; 2 | -------------------------------------------------------------------------------- /src/lib/extend/safe_curl.ts: -------------------------------------------------------------------------------- 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; 8 | export type HttpClientRequestURL = HttpClientParameters[0]; 9 | export type HttpClientOptions = HttpClientParameters[1] & { checkAddress?: SSRFCheckAddressFunction }; 10 | export type HttpClientResponse = Awaited> & { data: T }; 11 | 12 | /** 13 | * safe curl with ssrf protection 14 | */ 15 | export async function safeCurlForApplication(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; 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(url, options); 32 | } 33 | 34 | return await app.curl(url, options); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/helper/cliFilter.ts: -------------------------------------------------------------------------------- 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 | } 21 | -------------------------------------------------------------------------------- /src/lib/helper/escape.ts: -------------------------------------------------------------------------------- 1 | import escapeHTML from 'escape-html'; 2 | 3 | export default escapeHTML; 4 | -------------------------------------------------------------------------------- /src/lib/helper/escapeShellArg.ts: -------------------------------------------------------------------------------- 1 | export default function escapeShellArg(text: string) { 2 | const str = '' + text; 3 | return '\'' + str.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'') + '\''; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/helper/escapeShellCmd.ts: -------------------------------------------------------------------------------- 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 | } 17 | -------------------------------------------------------------------------------- /src/lib/helper/index.ts: -------------------------------------------------------------------------------- 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 | }; 22 | -------------------------------------------------------------------------------- /src/lib/helper/shtml.ts: -------------------------------------------------------------------------------- 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 | } 78 | -------------------------------------------------------------------------------- /src/lib/helper/sjs.ts: -------------------------------------------------------------------------------- 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 = { 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 | } 58 | -------------------------------------------------------------------------------- /src/lib/helper/sjson.ts: -------------------------------------------------------------------------------- 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 | } 36 | -------------------------------------------------------------------------------- /src/lib/helper/spath.ts: -------------------------------------------------------------------------------- 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 | } 28 | -------------------------------------------------------------------------------- /src/lib/helper/surl.ts: -------------------------------------------------------------------------------- 1 | import type { BaseContextClass } from '@eggjs/core'; 2 | 3 | const escapeMap: Record = { 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 | } 36 | -------------------------------------------------------------------------------- /src/lib/middlewares/csp.ts: -------------------------------------------------------------------------------- 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 | }; 71 | -------------------------------------------------------------------------------- /src/lib/middlewares/csrf.ts: -------------------------------------------------------------------------------- 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 | }; 45 | -------------------------------------------------------------------------------- /src/lib/middlewares/dta.ts: -------------------------------------------------------------------------------- 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 | }; 14 | -------------------------------------------------------------------------------- /src/lib/middlewares/hsts.ts: -------------------------------------------------------------------------------- 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 | }; 25 | -------------------------------------------------------------------------------- /src/lib/middlewares/index.ts: -------------------------------------------------------------------------------- 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 | }; 24 | -------------------------------------------------------------------------------- /src/lib/middlewares/methodnoallow.ts: -------------------------------------------------------------------------------- 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 = {}; 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 | }; 24 | -------------------------------------------------------------------------------- /src/lib/middlewares/noopen.ts: -------------------------------------------------------------------------------- 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 | }; 19 | -------------------------------------------------------------------------------- /src/lib/middlewares/nosniff.ts: -------------------------------------------------------------------------------- 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 = { 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 | }; 33 | -------------------------------------------------------------------------------- /src/lib/middlewares/referrerPolicy.ts: -------------------------------------------------------------------------------- 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 | }; 40 | -------------------------------------------------------------------------------- /src/lib/middlewares/xframe.ts: -------------------------------------------------------------------------------- 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 | }; 21 | -------------------------------------------------------------------------------- /src/lib/middlewares/xssProtection.ts: -------------------------------------------------------------------------------- 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 | }; 18 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 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} 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 = {}; 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, opts?: Record) { 96 | if (!opts) { 97 | return origin; 98 | } 99 | const res: Record = {}; 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 | } 209 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 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 | } 17 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | // make sure to import egg typings and let typescript know about it 2 | // @see https://github.com/whxaxes/blog/issues/11 3 | // and https://www.typescriptlang.org/docs/handbook/declaration-merging.html 4 | import 'egg'; 5 | -------------------------------------------------------------------------------- /test/app/extends/cliFilter.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/cliFilter.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-cliFilter-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | after(mm.restore); 15 | 16 | describe('helper.cliFilter()', () => { 17 | it('should convert special chars in param and not convert chars in whitelists', () => { 18 | return app.httpRequest() 19 | .get('/cliFilter') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should not convert when chars in whitelists', () => { 25 | return app.httpRequest() 26 | .get('/cliFilter-2') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/app/extends/escapeShellArg.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/escapeShellArg.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-escapeShellArg-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | after(mm.restore); 15 | 16 | describe('helper.escapeShellArg()', () => { 17 | it('should add single quotes around a string', () => { 18 | return app.httpRequest() 19 | .get('/escapeShellArg') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should add single quotes around a string and quotes/escapes any existing single quotes', () => { 25 | return app.httpRequest() 26 | .get('/escapeShellArg-2') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | 31 | it('should not affect normal arg', () => { 32 | return app.httpRequest() 33 | .get('/escapeShellArg-3') 34 | .expect(200) 35 | .expect('true'); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/app/extends/escapeShellCmd.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/escapeShellCmd.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-escapeShellCmd-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | afterEach(mm.restore); 15 | 16 | describe('helper.escapeShellCmd()', () => { 17 | it('should convert chars in blacklists', () => { 18 | return app.httpRequest() 19 | .get('/escapeShellCmd') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should not affect normal cmd', () => { 25 | return app.httpRequest() 26 | .get('/escapeShellCmd-2') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/app/extends/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/app/extends/helper.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | let app3: MockApplication; 8 | before(async () => { 9 | app = mm.app({ 10 | baseDir: 'apps/helper-app', 11 | }); 12 | await app.ready(); 13 | 14 | app2 = mm.app({ 15 | baseDir: 'apps/helper-config-app', 16 | }); 17 | await app2.ready(); 18 | 19 | app3 = mm.app({ 20 | baseDir: 'apps/helper-link-app', 21 | }); 22 | await app3.ready(); 23 | }); 24 | 25 | after(async () => { 26 | await app.close(); 27 | await app2.close(); 28 | await app3.close(); 29 | }); 30 | 31 | afterEach(mm.restore); 32 | 33 | describe('helper.escape()', () => { 34 | it('should work', () => { 35 | return app.httpRequest() 36 | .get('/escape') 37 | .expect(200) 38 | .expect('true'); 39 | }); 40 | }); 41 | 42 | describe('helper.shtml()', () => { 43 | it('should basic usage work', () => { 44 | return app.httpRequest() 45 | .get('/shtml-basic') 46 | .expect(200) 47 | .expect('true'); 48 | }); 49 | 50 | it('should escape tag not in default whitelist', () => { 51 | return app.httpRequest() 52 | .get('/shtml-escape-tag-not-in-default-whitelist') 53 | .expect(200) 54 | .expect('true'); 55 | }); 56 | 57 | it('should support multiple filter', () => { 58 | return app.httpRequest() 59 | .get('/shtml-multiple-filter') 60 | .expect(200) 61 | .expect('true'); 62 | }); 63 | 64 | it('should escape script', () => { 65 | return app.httpRequest() 66 | .get('/shtml-escape-script') 67 | .expect(200) 68 | .expect('true'); 69 | }); 70 | 71 | it('should escape img onload', () => { 72 | return app.httpRequest() 73 | .get('/shtml-escape-img-onload') 74 | .expect(200) 75 | .expect('true'); 76 | }); 77 | 78 | it('should escape hostname null', () => { 79 | return app.httpRequest() 80 | .get('/shtml-escape-hostname-null') 81 | .expect(200) 82 | .expect('true'); 83 | }); 84 | 85 | it('should support configuration', () => { 86 | return app2.httpRequest() 87 | .get('/shtml-configuration') 88 | .expect(200) 89 | .expect('true'); 90 | }); 91 | 92 | it('should ignore domains not in default domainList', () => { 93 | return app.httpRequest() 94 | .get('/shtml-ignore-domains-not-in-default-domainList') 95 | .expect(200) 96 | .expect('true'); 97 | }); 98 | 99 | it('should ignore hash', () => { 100 | return app3.httpRequest() 101 | .get('/shtml-ignore-hash') 102 | .expect(200) 103 | .expect('true'); 104 | }); 105 | 106 | it('should support extending domainList via config.helper.shtml.domainWhiteList', () => { 107 | return app2.httpRequest() 108 | .get('/shtml-extending-domainList-via-config.helper.shtml.domainWhiteList') 109 | .expect(200) 110 | .expect('true'); 111 | }); 112 | 113 | it('should support absolute path', () => { 114 | return app.httpRequest() 115 | .get('/shtml-absolute-path') 116 | .expect(200) 117 | .expect('true'); 118 | }); 119 | 120 | it('should stripe css url', () => { 121 | return app2.httpRequest() 122 | .get('/shtml-stripe-css-url') 123 | .expect(200) 124 | .expect('true'); 125 | }); 126 | 127 | it('should customize whitelist via this.securityOptions.shtml', () => { 128 | return app.httpRequest() 129 | .get('/shtml-custom-via-security-options') 130 | .expect(200) 131 | .expect('true'); 132 | }); 133 | 134 | it('should check securityOptions when call shtml directly', () => { 135 | const ctx = app.mockContext(); 136 | assert.equal(ctx.helper.shtml('
'), '
'); 137 | }); 138 | }); 139 | 140 | describe('helper.sjs()', () => { 141 | it('should sjs(foo) work', () => { 142 | return app.httpRequest() 143 | .get('/sjs') 144 | .expect(200) 145 | .expect('true'); 146 | }); 147 | 148 | it('should convert special chars on js context', () => { 149 | return app.httpRequest() 150 | .get('/sjs-2') 151 | .expect(200) 152 | .expect('true'); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/app/extends/sjs.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/sjs.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-sjs-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | afterEach(mm.restore); 15 | 16 | describe('helper.sjs()', () => { 17 | it('should convert special chars on js context and not convert chart in whitelists', () => { 18 | return app.httpRequest() 19 | .get('/sjs') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should not convert when chars in whitelists', () => { 25 | return app.httpRequest() 26 | .get('/sjs-2') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | 31 | it('should convert all special chars on js context except for special', () => { 32 | return app.httpRequest() 33 | .get('/sjs-3') 34 | .expect(200) 35 | .expect('true'); 36 | }); 37 | 38 | it('should only convert special chars plus /', () => { 39 | return app.httpRequest() 40 | .get('/sjs-4') 41 | .expect(200) 42 | .expect('true'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/app/extends/sjson.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/sjson.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-sjson-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | afterEach(mm.restore); 15 | 16 | describe('helper.sjson()', () => { 17 | it('should not convert json string when json is safe', () => { 18 | return app.httpRequest() 19 | .get('/safejson') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should not convert json string when json is safe contains array', () => { 25 | return app.httpRequest() 26 | .get('/safejsontc2') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | it('should not convert json string when json is safe contains string', () => { 31 | return app.httpRequest() 32 | .get('/safejsontc3') 33 | .expect(200) 34 | .expect('true'); 35 | }); 36 | it('should not convert json string when json is safe contains object', () => { 37 | return app.httpRequest() 38 | .get('/safejsontc4') 39 | .expect(200) 40 | .expect('true'); 41 | }); 42 | it('should not convert json string when json is safe contains symbel', () => { 43 | return app.httpRequest() 44 | .get('/safejsontc5') 45 | .expect(200) 46 | .expect('true'); 47 | }); 48 | it('should not convert json string when json is safe contains function', () => { 49 | return app.httpRequest() 50 | .get('/safejsontc6') 51 | .expect(200) 52 | .expect('true'); 53 | }); 54 | it('should not convert json string when json is safe contains buffer', () => { 55 | return app.httpRequest() 56 | .get('/safejsontc7') 57 | .expect(200) 58 | .expect('true'); 59 | }); 60 | it('should not convert json string when json is safe contains null', () => { 61 | return app.httpRequest() 62 | .get('/safejsontc8') 63 | .expect(200) 64 | .expect('true'); 65 | }); 66 | it('should not convert json string when json is safe contains undefined', () => { 67 | return app.httpRequest() 68 | .get('/safejsontc9') 69 | .expect(200) 70 | .expect('true'); 71 | }); 72 | it('should not convert json string when json is safe contains boolean', () => { 73 | return app.httpRequest() 74 | .get('/safejsontc10') 75 | .expect(200) 76 | .expect('true'); 77 | }); 78 | 79 | it('should convert json string when json contains unsafe key or value', () => { 80 | return app.httpRequest() 81 | .get('/unsafejson') 82 | .expect(200) 83 | .expect('true'); 84 | }); 85 | 86 | it('should convert json string when json contains unsafe value nested', () => { 87 | return app.httpRequest() 88 | .get('/unsafejson2') 89 | .expect(200) 90 | .expect('true'); 91 | }); 92 | 93 | it('should convert json string when json contains unsafe value nested in array', () => { 94 | return app.httpRequest() 95 | .get('/unsafejson3') 96 | .expect(200) 97 | .expect('true'); 98 | }); 99 | 100 | it('should convert json string when json contains unsafe key nested in array', () => { 101 | return app.httpRequest() 102 | .get('/unsafejson4') 103 | .expect(200) 104 | .expect('true'); 105 | }); 106 | 107 | it('should convert json string when json contains unsafe key', () => { 108 | return app.httpRequest() 109 | .get('/unsafejson5') 110 | .expect(200) 111 | .expect('true'); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/app/extends/spath.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/app/extends/spath.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/helper-spath-app', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | after(() => app.close()); 13 | 14 | after(mm.restore); 15 | 16 | describe('helper.spath()', () => { 17 | it('should pass when filepath is safe', () => { 18 | return app.httpRequest() 19 | .get('/safepath') 20 | .expect(200) 21 | .expect('true'); 22 | }); 23 | 24 | it('should return null when filepath is not safe(contains ..)', () => { 25 | return app.httpRequest() 26 | .get('/unsafepath') 27 | .expect(200) 28 | .expect('true'); 29 | }); 30 | 31 | it('should return null when filepath is not safe(contains /)', () => { 32 | return app.httpRequest() 33 | .get('/unsafepath2') 34 | .expect(200) 35 | .expect('true'); 36 | }); 37 | 38 | it('should decode first when filepath contains %', () => { 39 | return app.httpRequest() 40 | .get('/unsafepath3') 41 | .expect(200) 42 | .expect('true'); 43 | }); 44 | 45 | it('should decode until filepath does not contains %', () => { 46 | return app.httpRequest() 47 | .get('/unsafepath4') 48 | .expect(200) 49 | .expect('true'); 50 | }); 51 | 52 | it('should not affect function when filepath decoding failed', () => { 53 | return app.httpRequest() 54 | .get('/unsafepath5') 55 | .expect(200) 56 | .expect('true'); 57 | }); 58 | 59 | it('should return source code when filepath argument is not a string', () => { 60 | return app.httpRequest() 61 | .get('/unsafepath6') 62 | .expect(200) 63 | .expect('true'); 64 | }); 65 | 66 | it('should return source path when filepath contained % but judged to be safe', () => { 67 | return app.httpRequest() 68 | .get('/unsafepath7') 69 | .expect(200) 70 | .expect('true'); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const benchmarks = require('beautify-benchmark'); 5 | const sjsHelper = require('../lib/helper/sjs'); 6 | const shtmlHelper = require('../lib/helper/shtml'); 7 | const surlHelper = require('../lib/helper/surl'); 8 | const spathHelper = require('../lib/helper/spath'); 9 | const sjsonHelper = require('../lib/helper/sjson'); 10 | const mm = require('egg-mock'); 11 | const app = mm.app({ 12 | baseDir: 'apps/helper-app', 13 | plugin: 'security', 14 | }); 15 | const ctx = app.mockContext(); 16 | ctx.ctx = {}; 17 | ctx.ctx.coreLogger = { 18 | warn() {}, 19 | }; 20 | const suite = new Benchmark.Suite(); 21 | const tc1 = '"hello"123abc"'; 22 | let tc2 = ''; 23 | 24 | for (let i = 0, l = 128; i < l; i++) { 25 | 26 | if (i === 9 || i === 10 || i === 13 || i > 47 && i < 58 || i > 64 && i < 91 || i > 96 && i < 123) { 27 | continue; 28 | } else { 29 | tc2 += String.fromCharCode(i); 30 | } 31 | 32 | } 33 | 34 | const tc3 = '

xx

'; 35 | const tc4 = '

Hello

'; 36 | const tc5 = '

Hello

'; 37 | const tc6 = '

Hello

'; 38 | const tc7 = '

Hello

'; 39 | const tc8 = 'altxx'; 40 | const tc9 = 'altxx'; 41 | const tc10 = '/////foo.com/'; 42 | const tc11 = 'xxx://xss.com'; 43 | const tc12 = '2.jpg'; 44 | const tc13 = '../home/admin'; 45 | 46 | const tc14 = { 47 | a: 1, 48 | }; 49 | const tc15 = { 50 | a: '', 51 | }; 52 | const tc16 = { 53 | a: { 54 | b: { 55 | c: { 56 | d: '', 57 | }, 58 | }, 59 | }, 60 | }; 61 | const tc17 = { 62 | a: { 63 | b: { 64 | c: { 65 | '', { 66 | e: '') == '

Hello

<script>alert(1)</script>'; 20 | }); 21 | 22 | app.get('/shtml-escape-img-onload', async function() { 23 | this.body = this.helper.shtml('

Hello

') == '

Hello

'; 24 | }); 25 | 26 | app.get('/shtml-escape-hostname-null', async function() { 27 | this.body = this.helper.shtml('test') == 'test'; 28 | }); 29 | 30 | app.get('/shtml-ignore-domains-not-in-default-domainList', async function() { 31 | this.body = this.helper.shtml('altxx') == 'altxx'; 32 | }); 33 | 34 | app.get('/shtml-absolute-path', async function() { 35 | this.body = this.helper.shtml('altxx') == 'altxx'; 36 | }); 37 | 38 | app.get('/shtml-custom-via-security-options', async function() { 39 | this.securityOptions.shtml = { 40 | whiteList: { 41 | video: ['src'], 42 | }, 43 | }; 44 | this.body = this.helper.shtml('
') === '<div src="xx"></div>'; 45 | }); 46 | 47 | app.get('/sjs', async function() { 48 | const foo = '"hello"'; 49 | this.body = `var foo = "${foo}"; var foo = "${this.helper.sjs(foo)}";`==='var foo = ""hello""; var foo = "\\x22hello\\x22";'; 50 | }); 51 | 52 | app.get('/sjs-2', async function() { 53 | const foo = '"hello\'\\()<>.'; 54 | this.body = `${this.helper.sjs(foo)}`==='\\x22hello\\x27\\x5c\\x28\\x29\\x3c\\x3e\\x2e'; 55 | }); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-app/config/config.js: -------------------------------------------------------------------------------- 1 | exports.keys = 'test key'; 2 | 3 | exports.security = { 4 | domainWhiteList:['.domain.com'], 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-app" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/helper-cliFilter-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/cliFilter', async function() { 3 | const port = '8889|chmod 777 /tmp/muma.sh;'; 4 | this.body = `cp.exec('./start.sh '+${this.helper.cliFilter(port)})` === 'cp.exec(\'./start.sh \'+8889chmod777tmpmuma.sh)'; 5 | }); 6 | 7 | app.get('/cliFilter-2', async function() { 8 | const port = '8889'; 9 | this.body = `${this.helper.cliFilter(port)}` === '8889'; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-cliFilter-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-cliFilter-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-sjs-app" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-config-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/shtml-configuration', async function() { 3 | this.body = this.helper.shtml('

Hello

aa') == '<h1>Hello</h1>aa'; 4 | }); 5 | 6 | app.get('/shtml-extending-domainList-via-config.helper.shtml.domainWhiteList', async function() { 7 | this.body = this.helper.shtml('altxx') == 'xx'; 8 | }); 9 | 10 | app.get('/shtml-stripe-css-url', async function() { 11 | this.body = this.helper.shtml('

xx

') == '

xx

'; 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-config-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.helper = { 6 | shtml: { 7 | whiteList: { 8 | a: ["title", "src"], 9 | img: ["src"], 10 | h2: ["style"], 11 | }, 12 | domainWhiteList: ['.shaoshuai.me'], 13 | }, 14 | }; 15 | exports.security = { 16 | domainWhiteList:['.domain.com'], 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-config-app/helper-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/shtml-basic', async function() { 3 | this.body = this.helper.shtml('

xx

') == '

xx

'; 4 | }); 5 | 6 | app.get('/shtml-escape-tag-not-in-default-whitelist', async function() { 7 | this.body = this.helper.shtml('

Hello

') == '<html>

Hello

</html>'; 8 | }); 9 | 10 | app.get('/shtml-multiple-filter', async function() { 11 | this.body = this.helper.shtml(this.helper.shtml('

Hello

')) == '<html>

Hello

</html>'; 12 | }); 13 | 14 | app.get('/shtml-escape-script', async function() { 15 | this.body = this.helper.shtml('

Hello

') == '

Hello

<script>alert(1)</script>'; 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-config-app/helper-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-app" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/helper-config-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-config-app" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellArg-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/escapeShellArg', async function() { 3 | const port = '8889|chmod 777 /tmp/muma.sh;'; 4 | this.body = `cp.exec('./start.sh '+${this.helper.escapeShellArg(port)})` === 'cp.exec(\'./start.sh \'+\'8889|chmod 777 /tmp/muma.sh;\')'; 5 | }); 6 | 7 | app.get('/escapeShellArg-2', async function() { 8 | const port = '8889\'|chmod 777 /tmp/muma.sh;echo \\'; 9 | this.body = `cp.exec('./start.sh '+${this.helper.escapeShellArg(port)})` === 'cp.exec(\'./start.sh \'+\'8889\\\'|chmod 777 /tmp/muma.sh;echo \\\\\')'; 10 | }); 11 | 12 | app.get('/escapeShellArg-3', async function() { 13 | const port = '8889'; 14 | this.body = `${this.helper.escapeShellArg(port)}` === '\'8889\''; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellArg-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellArg-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-sjs-app" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellCmd-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/escapeShellCmd', async function() { 3 | const port = '8889|chmod 777 /tmp/muma.sh;'; 4 | this.body = `cp.exec('./start.sh '+${this.helper.escapeShellCmd(port)})` === 'cp.exec(\'./start.sh \'+8889chmod 777 /tmp/muma.sh)'; 5 | }); 6 | 7 | app.get('/escapeShellCmd-2', async function() { 8 | const port = '-Pn -A -sT 8889'; 9 | this.body = `${this.helper.escapeShellCmd(port)}` === '-Pn -A -sT 8889'; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellCmd-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-escapeShellCmd-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-sjs-app" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-link-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/shtml-ignore-hash', async function() { 3 | this.body = this.helper.shtml('xx') == 'xx'; 4 | }); 5 | 6 | app.get('/shtml-not-in-whitelist', async function() { 7 | this.body = this.helper.shtml('xx') == 'xx'; 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-link-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.helper = { 6 | shtml: { 7 | whiteList: { 8 | a: ["href"], 9 | }, 10 | domainWhiteList: ['.shaoshuai.me'], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-link-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-link-app" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-sjs-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/sjs', async function() { 3 | const foo = '"hello"123abc"'; 4 | this.body = `var foo = "${foo}"; var foo = "${this.helper.sjs(foo)}";` === 'var foo = ""hello"123abc""; var foo = "\\x22hello\\x22123abc\\x22";'; 5 | }); 6 | 7 | app.get('/sjs-2', async function() { 8 | 9 | let foo = ''; 10 | let res = '' 11 | 12 | for (let i = 0, l = 128; i < l; i++) { 13 | 14 | if (i > 47 && i < 58 || i > 64 && i < 91 || i > 96 && i < 123) { 15 | foo += String.fromCharCode(i); 16 | res += String.fromCharCode(i); 17 | } else { 18 | 19 | } 20 | 21 | } 22 | 23 | this.body = `${this.helper.sjs(foo)}` === res; 24 | }); 25 | 26 | app.get('/sjs-3', async function() { 27 | 28 | let foo = ''; 29 | let res = '' 30 | 31 | for (let i = 0, l = 128; i < l; i++) { 32 | 33 | if (i == 9 || i == 10 || i == 13 || i > 47 && i < 58 || i > 64 && i < 91 || i > 96 && i < 123) { 34 | 35 | } else { 36 | foo += String.fromCharCode(i); 37 | res += '\\x' + i.toString(16); 38 | } 39 | 40 | } 41 | 42 | this.body = `${this.helper.sjs(foo)}` === res; 43 | }); 44 | 45 | app.get('/sjs-4', async function() { 46 | 47 | const map = { 48 | '\t': '\\t', 49 | '\n': '\\n', 50 | '\r': '\\r', 51 | }; 52 | let foo = ''; 53 | let res = '' 54 | 55 | for (let i = 0, l = 128; i < l; i++) { 56 | 57 | if (i == 9 || i == 10 || i == 13) { 58 | 59 | foo += String.fromCharCode(i); 60 | res += map[String.fromCharCode(i)]; 61 | 62 | } 63 | 64 | if (i == 9 || i == 10 || i == 13 || i > 47 && i < 58 || i > 64 && i < 91 || i > 96 && i < 123) { 65 | 66 | } else { 67 | foo += String.fromCharCode(i); 68 | res += '\\x' + i.toString(16); 69 | } 70 | 71 | } 72 | this.body = `${this.helper.sjs(foo)}` === res; 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-sjs-app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-sjs-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-sjs-app" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/helper-sjson-app/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/safejson', async function() { 3 | const obj = { 4 | a: 1 5 | }; 6 | this.body = `var foo = "${this.helper.sjson(obj)}"` === 'var foo = "{"a":1}"'; 7 | }); 8 | 9 | app.get('/unsafejson', async function() { 10 | const obj2 = { 11 | a: '' 12 | }; 13 | this.body = `${this.helper.sjson(obj2)}` === '{"a":"\\\\x3cscript\\\\x20type\\\\x3d\\\\x22sdfdsd\\\\x22\\\\x3ealert\\\\x28111\\\\x29\\\\x3c\\\\x2fscript\\\\x3e"}'; 14 | }); 15 | 16 | app.get('/unsafejson2', async function() { 17 | const obj3 = { 18 | a: { 19 | b: { 20 | c: { 21 | d: '' 22 | } 23 | } 24 | } 25 | }; 26 | this.body = `${this.helper.sjson(obj3)}` === '{"a":{"b":{"c":{"d":"\\\\x3cscript\\\\x3e\\\\x3f\\\\x3c\\\\x2fscript\\\\x3e"}}}}'; 27 | }); 28 | 29 | app.get('/unsafejson3', async function() { 30 | const obj4 = { 31 | a: { 32 | b: { 33 | c: { 34 | d: ['', { 35 | e: '', { 50 | e: ''; 31 | this.body = app.injectNonce(bodyString4); 32 | }); 33 | app.get('/testrender', async function() { 34 | this.set('x-csrf', this.csrf); 35 | await this.render('index.nj', {}); 36 | }); 37 | app.get('/testispInjection', async function() { 38 | 39 | const injectDefenceHtml = app.injectHijackingDefense(mockHtml); 40 | 41 | function mockInject(html){ 42 | const injectScript = ''; 43 | const regHackByStart = /([\S\s]*?)<\/html>/; 44 | return html.replace(regHackByStart,function($0,$1){ 45 | return $1 +injectScript+'' 46 | }); 47 | } 48 | // console.log(mockInject(mockHtml)); 49 | this.body = await this.renderString(mockInject(injectDefenceHtml), this); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /test/fixtures/apps/inject/app/view/index.nj: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/apps/inject/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.view = { 6 | defaultViewEngine: 'nunjucks', 7 | mapping: { 8 | '.nj': 'nunjucks', 9 | }, 10 | }; 11 | 12 | exports.security = { 13 | defaultMiddleware: 'csp', 14 | csp: { 15 | enable: true, 16 | policy: { 17 | 'script-src': [ 18 | '\'self\'', 19 | '\'unsafe-inline\'', 20 | '\'unsafe-eval\'', 21 | 'www.google-analytics.com', 22 | ], 23 | 'style-src': [ 24 | '\'unsafe-inline\'', 25 | 'www.google-analytics.com', 26 | ], 27 | 'img-src': [ 28 | '\'self\'', 29 | 'data:', 30 | 'www.google-analytics.com', 31 | ], 32 | 'frame-ancestors': [ 33 | '\'self\'', 34 | ], 35 | 'report-uri': 'http://pointman.domain.com/csp?app=csp', 36 | }, 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/fixtures/apps/inject/config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.nunjucks = { 4 | enable: true, 5 | package: 'egg-view-nunjucks', 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/apps/inject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inject" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain-custom/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | const customWhiteList = [ 3 | '*.foo.com', 4 | '*.bar.net', 5 | ]; 6 | 7 | app.get('/unsafe', async function() { 8 | const unsafeDomains = [ 9 | // unsafe 10 | 'aAa-domain.com', 11 | '192.1.168.0', 12 | 'http://www.baidu.com/zh-CN', 13 | 'www.alimama.com', 14 | 'foo.com.cn', 15 | 'a.foo.com.cn', 16 | 17 | // safe 18 | 'pre-www.foo.com', 19 | 'pre-www.bar.net', 20 | ]; 21 | let unsafeCounter = 0; 22 | for (let unsafeDomain of unsafeDomains) { 23 | if (!this.isSafeDomain(unsafeDomain, customWhiteList)) { 24 | unsafeCounter++; 25 | } 26 | } 27 | 28 | this.body = unsafeCounter === 6 ? false : true; 29 | }); 30 | 31 | app.get('/safe', async function() { 32 | const safeDomains = [ 33 | 'a.foo.com', 34 | 'a.b.foo.com', 35 | 'a.b.c.foo.com', 36 | 'pre-www.foo.com', 37 | 'test.pre-www.foo.com', 38 | 'a.bar.net', 39 | 'a.b.bar.net', 40 | 'a.b.c.bar.net', 41 | 'pre-www.bar.net', 42 | 'test.pre-www.bar.net', 43 | ]; 44 | let safeCounter = 0; 45 | 46 | for (const safeDomain of safeDomains) { 47 | if (this.isSafeDomain(safeDomain, customWhiteList)) { 48 | safeCounter++; 49 | } 50 | } 51 | 52 | this.body = safeCounter === 10; 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain-custom/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xframe', 7 | domainWhiteList: ['.domain.com', 'http://www.baidu.com', '192.*.0.*', '*.alibaba.com'], 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | app.get('/', async function() { 3 | const unsafeDomains = ['aAa-domain.com', '192.1.168.0', 'http://www.baidu.com/zh-CN', 'www.alimama.com']; 4 | let unsafeCounter = 0; 5 | for (let unsafeDomain of unsafeDomains) { 6 | if (!this.isSafeDomain(unsafeDomain)) { 7 | unsafeCounter++; 8 | } 9 | } 10 | this.body = unsafeCounter === 4 ? false : true; 11 | }); 12 | app.get('/safe', async function() { 13 | const safeDomains = ['wWw.domain.com', '192.1.0.255', 'http://www.BaIDu.com', 'wwW.alIbAbA.com']; 14 | let safeCounter = 0; 15 | for (let safeDomain of safeDomains) { 16 | if (this.isSafeDomain(safeDomain)) { 17 | safeCounter++; 18 | } 19 | } 20 | this.body = safeCounter === 4; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xframe', 7 | domainWhiteList: ['.domain.com', 'http://www.baidu.com', '192.*.0.*', '*.alibaba.com'], 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/isSafeDomain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/method/app/router.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('node:http'); 2 | 3 | module.exports = function(app) { 4 | METHODS.forEach(function(m) { 5 | m = m.toLowerCase(); 6 | app.router[m] && app.router[m]('/', async function() { 7 | this.body = '123'; 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/method/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'methodnoallow' 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/method/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "method_not_allow" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/noopen/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = '123'; 4 | }); 5 | 6 | app.get('/disable', function(){ 7 | this.securityOptions.noopen = { enable: false }; 8 | this.body = '123'; 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/noopen/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'noopen', 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/noopen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noopen" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/nosniff/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function() { 3 | this.body = '123'; 4 | }); 5 | 6 | app.get('/disable', function() { 7 | this.securityOptions.nosniff = { enable: false }; 8 | this.body = '123'; 9 | }); 10 | 11 | app.get('/redirect', function() { 12 | this.redirect('/'); 13 | }); 14 | 15 | app.get('/redirect301', function() { 16 | this.status = 301; 17 | this.redirect('/'); 18 | }); 19 | 20 | app.get('/redirect307', function() { 21 | this.status = 307; 22 | this.redirect('/'); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/apps/nosniff/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'nosniff' 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/nosniff/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nosniff" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config-compatibility/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function() { 3 | this.body = '123'; 4 | }); 5 | app.get('/referrer', function() { 6 | const policy = this.query.policy; 7 | this.body = '123'; 8 | this.securityOptions.refererPolicy = { 9 | enable: true, 10 | value: policy 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config-compatibility/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'referrerPolicy', 7 | referrerPolicy: { 8 | value: 'origin', 9 | enable: true 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config-compatibility/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "referrer-config" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function() { 3 | this.body = '123'; 4 | }); 5 | app.get('/referrer', function() { 6 | const policy = this.query.policy; 7 | this.body = '123'; 8 | this.securityOptions.referrerPolicy = { 9 | enable: true, 10 | value: policy 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'referrerPolicy', 7 | referrerPolicy: { 8 | value: 'origin', 9 | enable: true 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "referrer-config" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = '123'; 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'referrerPolicy', 7 | referrerPolicy: { 8 | enable: true 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/referrer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "referrer" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.safeRedirect = async function() { 2 | const goto = this.query.goto; 3 | console.log('%j, %s', goto, goto); 4 | this.redirect(goto); 5 | }; 6 | 7 | exports.unSafeRedirect = async function() { 8 | const goto = this.query.goto; 9 | this.unsafeRedirect(goto); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | app.get('/safe_redirect', app.controller.home.safeRedirect); 5 | app.get('/unsafe_redirect', app.controller.home.unSafeRedirect); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | csrf: { 7 | enable: false, 8 | }, 9 | domainWhiteList:['.domain.com'], 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe_redirect" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect_noconfig/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.safeRedirect = async function() { 2 | const goto = this.query.goto; 3 | this.redirect(goto); 4 | }; 5 | 6 | exports.unSafeRedirect = async function() { 7 | const goto = this.query.goto; 8 | this.unsafeRedirect(goto); 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect_noconfig/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | app.get('/safe_redirect', app.controller.home.safeRedirect); 5 | app.get('/unsafe_redirect', app.controller.home.unSafeRedirect); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect_noconfig/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | ctoken: { 7 | enable: false, 8 | }, 9 | 10 | csrf: { 11 | enable: false, 12 | }, 13 | domainWhiteList:[], 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/safe_redirect_noconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe_redirect_noconfig" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-controller/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | delete this.response.header['Strict-Transport-Security']; 4 | delete this.response.header['X-Download-Options']; 5 | delete this.response.header['X-Content-Type-Options']; 6 | delete this.response.header['X-XSS-Protection']; 7 | this.body = this.isSafeDomain('aaa-domain.com'); 8 | }); 9 | app.get('/safe', function(){ 10 | this.body = this.isSafeDomain('www.domain.com'); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-controller/config/config.js: -------------------------------------------------------------------------------- 1 | 2 | exports.keys = 'test key'; 3 | 4 | exports.security = { 5 | hsts: { 6 | enable: true 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-middleware/app/middleware/override.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return async (ctx, next) => { 3 | delete ctx.response.header['Strict-Transport-Security']; 4 | delete ctx.response.header['X-Download-Options']; 5 | delete ctx.response.header['X-Content-Type-Options']; 6 | delete ctx.response.header['X-XSS-Protection']; 7 | await next(); 8 | delete ctx.response.header['Strict-Transport-Security']; 9 | delete ctx.response.header['X-Download-Options']; 10 | delete ctx.response.header['X-Content-Type-Options']; 11 | delete ctx.response.header['X-XSS-Protection']; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-middleware/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = this.isSafeDomain('aaa-domain.com'); 4 | }); 5 | app.get('/safe', function(){ 6 | this.body = this.isSafeDomain('www.domain.com'); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-middleware/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | // 自定义中间件 6 | exports.middleware = [ 7 | 'override', 8 | ]; 9 | 10 | exports.security = { 11 | hsts: { 12 | enable: true 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-override-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-unset/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = this.isSafeDomain('aaa-domain.com'); 4 | }); 5 | app.get('/safe', function(){ 6 | this.body = this.isSafeDomain('www.domain.com'); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-unset/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | xframe: { 7 | enable: false 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/apps/security-unset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/security/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = this.isSafeDomain('aaa-domain.com'); 4 | }); 5 | app.get('/safe', function(){ 6 | this.body = this.isSafeDomain('www.domain.com'); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/security/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/security/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isSafeDomain" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-check-address-useHttpClientNext/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.security = { 2 | ssrf: { 3 | ipBlackList: [ 4 | '10.0.0.0/8', 5 | '127.0.0.1', 6 | '0.0.0.0/32', 7 | ], 8 | checkAddress(ip) { 9 | return ip !== '127.0.0.2'; 10 | }, 11 | }, 12 | }; 13 | 14 | exports.httpclient = { 15 | useHttpClientNext: true, 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-check-address-useHttpClientNext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-ip-check-address-useHttpClientNext" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-check-address/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.security = { 4 | ssrf: { 5 | ipBlackList: [ 6 | '10.0.0.0/8', 7 | '127.0.0.1', 8 | '0.0.0.0/32', 9 | ], 10 | checkAddress(ip) { 11 | return ip !== '127.0.0.2'; 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-check-address/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-ip-check-address" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-hostname-exception-list/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.security = { 4 | ssrf: { 5 | ipBlackList: [ 6 | '10.0.0.0/8', 7 | '127.0.0.1', 8 | '0.0.0.0/32', 9 | ], 10 | hostnameExceptionList: [ 11 | 'registry.npmjs.org', 12 | 'registry.npmmirror.com', 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-hostname-exception-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-ip-black-list" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-ip-black-list/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.security = { 4 | ssrf: { 5 | ipBlackList: [ 6 | '10.0.0.0/8', 7 | '127.0.0.1', 8 | '0.0.0.0/32', 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-ip-black-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-ip-black-list" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-ip-exception-list/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.security = { 4 | ssrf: { 5 | ipBlackList: [ 6 | '10.0.0.0/8', 7 | '127.0.0.1', 8 | '0.0.0.0/32', 9 | ], 10 | ipExceptionList: [ 11 | '10.1.1.1', 12 | ], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/ssrf-ip-exception-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-ip-black-list" 3 | } -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/match', function(){ 3 | this.body = 'hello'; 4 | }); 5 | app.get('/luckydrq', function(){ 6 | this.body = 'hello'; 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'csp', 7 | match: /\/(?:match|ignore)/, 8 | csp: { 9 | enable: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass2/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/match', function(){ 3 | this.body = 'hello'; 4 | }); 5 | app.get('/mymatch', function(){ 6 | this.body = 'hello'; 7 | }); 8 | app.get('/mytrueignore', function(){ 9 | this.body = 'hello'; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass2/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'csp', 7 | match: /\/match/, 8 | csp: { 9 | match: /\/(?:mymatch|myignore)/, 10 | enable: true 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass2" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass3/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/ignore', function(){ 3 | this.body = 'hello'; 4 | }); 5 | app.get('/luckydrq', function(){ 6 | this.body = 'hello'; 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass3/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'csp', 7 | ignore: '/ignore', 8 | csp: { 9 | enable: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass3" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass4/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/ignore', function(){ 3 | this.body = 'hello'; 4 | }); 5 | app.get('/myignore', function(){ 6 | this.body = 'hello'; 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass4/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'csp', 7 | ignore: '/ignore', 8 | csp: { 9 | ignore: '/myignore', 10 | enable: true 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass4" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass5/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = 'xx'; 4 | }); 5 | app.get('/ignore1', function(){ 6 | this.body = 'xx'; 7 | }); 8 | app.get('/ignore2', function(){ 9 | this.body = 'xx'; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass5/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xframe', 7 | xframe: { 8 | ignore: ['/ignore1', '/ignore2'], 9 | enable: true 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass4" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass6/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = 'xx'; 4 | }); 5 | app.get('/match1', function(){ 6 | this.body = 'xx'; 7 | }); 8 | app.get('/match2', function(){ 9 | this.body = 'xx'; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass6/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xframe', 7 | xframe: { 8 | match: ['/match1', '/match2'], 9 | enable: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/utils-check-if-pass6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils-check-if-pass4" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close-zero/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = '123'; 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close-zero/config/config.js: -------------------------------------------------------------------------------- 1 | exports.keys = 'test key'; 2 | 3 | exports.security = { 4 | defaultMiddleware: 'xssProtection', 5 | xssProtection: { 6 | value: 0 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close-zero/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xss" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = '123'; 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xssProtection', 7 | xssProtection: { 8 | value: "0" 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss-close/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xss" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/', function(){ 3 | this.body = '123'; 4 | }); 5 | 6 | app.get('/0', function(){ 7 | this.securityOptions.xssProtection = { 8 | value: 0, 9 | }; 10 | this.body = '123'; 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'test key'; 4 | 5 | exports.security = { 6 | defaultMiddleware: 'xssProtection' 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/xss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xss" 3 | } 4 | -------------------------------------------------------------------------------- /test/hsts.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/hsts.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | let app3: MockApplication; 8 | describe('server', () => { 9 | before(async () => { 10 | app = mm.app({ 11 | baseDir: 'apps/hsts', 12 | }); 13 | await app.ready(); 14 | app2 = mm.app({ 15 | baseDir: 'apps/hsts-nosub', 16 | }); 17 | await app2.ready(); 18 | app3 = mm.app({ 19 | baseDir: 'apps/hsts-default', 20 | }); 21 | await app3.ready(); 22 | }); 23 | 24 | afterEach(mm.restore); 25 | 26 | after(async () => { 27 | await app.close(); 28 | await app2.close(); 29 | await app3.close(); 30 | }); 31 | 32 | it('should contain not Strict-Transport-Security header with default', async () => { 33 | const res = await app3.httpRequest() 34 | .get('/') 35 | .set('accept', 'text/html') 36 | .expect(200); 37 | assert.equal(res.headers['strict-transport-security'], undefined); 38 | }); 39 | 40 | it('should contain Strict-Transport-Security header when configured', () => { 41 | return app2.httpRequest() 42 | .get('/') 43 | .set('accept', 'text/html') 44 | .expect('Strict-Transport-Security', 'max-age=31536000') 45 | .expect(200); 46 | }); 47 | 48 | it('should contain includeSubdomains rule when defined', () => { 49 | return app.httpRequest() 50 | .get('/') 51 | .set('accept', 'text/html') 52 | .expect('Strict-Transport-Security', 'max-age=31536000; includeSubdomains') 53 | .expect(200); 54 | }); 55 | 56 | it('should not contain includeSubdomains rule with this.securityOptions', () => { 57 | return app.httpRequest() 58 | .get('/nosub') 59 | .set('accept', 'text/html') 60 | .expect('Strict-Transport-Security', 'max-age=31536000') 61 | .expect(200); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/inject.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/inject.test.ts', () => { 5 | let app: MockApplication; 6 | before(() => { 7 | app = mm.app({ 8 | baseDir: 'apps/inject', 9 | }); 10 | return app.ready(); 11 | }); 12 | 13 | after(() => app.close()); 14 | 15 | afterEach(mm.restore); 16 | 17 | describe('csrfInject', () => { 18 | it('should support inject csrf', async () => { 19 | const res = await app.httpRequest() 20 | .get('/testcsrf') 21 | .expect(200); 22 | assert.equal(res.text, '
\r\n
'); 23 | }); 24 | 25 | it('should not inject csrf when user write a csrf hidden area', async () => { 26 | const res = await app.httpRequest() 27 | .get('/testcsrf2') 28 | .expect(200); 29 | assert.equal(res.text, '
'); 30 | }); 31 | it('should not inject csrf when user write a csrf hidden area within a single dot area', async () => { 32 | const res = await app.httpRequest() 33 | .get('/testcsrf3') 34 | .expect(200); 35 | assert.equal(res.text, '
'); 36 | }); 37 | }); 38 | 39 | describe('nonceInject', () => { 40 | it('should inject nonce', async () => { 41 | const res = await app.httpRequest() 42 | .get('/testnonce') 43 | .expect(200); 44 | const body = res.text; 45 | const parts = body.split('|'); 46 | const expectedNonce = parts[0]; 47 | const scriptTag = parts[1]; 48 | assert.equal(scriptTag, ``); 49 | }); 50 | 51 | it('should not inject nonce when existed', async () => { 52 | const res = await app.httpRequest() 53 | .get('/testnonce2') 54 | .expect(200); 55 | assert.equal(res.text, ''); 56 | }); 57 | }); 58 | 59 | describe('IspInjectDefence', function() { 60 | it('should inject IspInjectDefence', async () => { 61 | const res = await app.httpRequest() 62 | .get('/testispInjection') 63 | .expect(200); 64 | assert.equal(res.text, '\n \n \n \n \n \n\n \n \n'); 65 | }); 66 | }); 67 | 68 | describe('work with view', function() { 69 | it('should successful render with csrf&nonce', async () => { 70 | const res = await app.httpRequest() 71 | .get('/testrender') 72 | .expect(200); 73 | const body = res.text; 74 | const header = res.headers['content-security-policy']; 75 | const csrf = res.headers['x-csrf']; 76 | const re_nonce = /nonce-([^']+)/; 77 | const nonce = header.match(re_nonce)![1]; 78 | assert(body.includes(nonce)); 79 | assert(body.includes(csrf)); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/lib/helper/surl.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/lib/helper/surl.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | 8 | before(async () => { 9 | app = mm.app({ 10 | baseDir: 'apps/helper-app', 11 | }); 12 | await app.ready(); 13 | }); 14 | 15 | before(async () => { 16 | app2 = mm.app({ 17 | baseDir: 'apps/helper-app-surlextend', 18 | }); 19 | await app2.ready(); 20 | }); 21 | 22 | afterEach(mm.restore); 23 | 24 | after(async () => { 25 | await app.close(); 26 | await app2.close(); 27 | }); 28 | 29 | it('should ignore hostname without protocol', () => { 30 | const ctx = app.mockContext(); 31 | assert.equal(ctx.helper.surl('foo.com'), ''); 32 | }); 33 | 34 | it('should support white protocol', () => { 35 | const ctx = app.mockContext(); 36 | assert.equal(ctx.helper.surl('http://foo.com/javascript:alert(/XSS/)'), 37 | 'http://foo.com/javascript:alert(/XSS/)'); 38 | assert.equal(ctx.helper.surl('https://foo.com/'), 'https://foo.com/'); 39 | assert.equal(ctx.helper.surl('https://foo.com/>'), 'https://foo.com/>'); 40 | assert.equal(ctx.helper.surl('file://foo.com/'), 'file://foo.com/'); 41 | assert.equal(ctx.helper.surl('file://fo { 51 | const ctx = app.mockContext(); 52 | assert.equal(ctx.helper.surl(123), 123); 53 | assert.equal(ctx.helper.surl(true), true); 54 | assert.equal(ctx.helper.surl('datad://foo.com'), ''); 55 | assert.equal(ctx.helper.surl('javascript1://foo.com'), ''); 56 | /* eslint-disable no-script-url */ 57 | assert.equal(ctx.helper.surl('javascript:alert(/XSS/)'), ''); 58 | assert.equal(ctx.helper.surl('xxx://xss.com'), ''); 59 | assert.equal(ctx.helper.surl('://xss.com'), ''); 60 | assert.equal(ctx.helper.surl('xss.com'), ''); 61 | assert.equal(ctx.helper.surl(' '), ''); 62 | assert.equal(ctx.helper.surl(' '), ''); 63 | assert.equal(ctx.helper.surl('\\\\ '), ''); 64 | assert.equal(ctx.helper.surl('\'">&bgPicUrl=https://cdn.com/images/giftprod/T1_GNfXfxXXXXXXXXX39e6601453bedfa5afee114ae1fa9bdd&_network=wifi&ttid=201200@laiwang_iphone_5.5.2'), ''); 65 | }); 66 | 67 | it('should support custom white protocol', () => { 68 | const ctx = app2.mockContext(); 69 | assert.equal(ctx.helper.surl('test://foo.com'), 'test://foo.com'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/method_not_allow.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/method_not_allow.test.ts', () => { 4 | let app: MockApplication; 5 | before(() => { 6 | app = mm.app({ 7 | baseDir: 'apps/method', 8 | }); 9 | return app.ready(); 10 | }); 11 | 12 | afterEach(mm.restore); 13 | 14 | after(() => app.close()); 15 | 16 | it('should allow', async () => { 17 | await app.httpRequest().get('/') 18 | .expect(200); 19 | }); 20 | 21 | it('should not allow trace method', async () => { 22 | await app.httpRequest() 23 | .trace('/') 24 | .set('accept', 'text/html') 25 | .expect(405); 26 | }); 27 | 28 | it('should allow options method', () => { 29 | return app.httpRequest() 30 | .options('/') 31 | .set('accept', 'text/html') 32 | .expect(200); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/noopen.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/noopen.test.ts', () => { 5 | let app: MockApplication; 6 | before(() => { 7 | app = mm.app({ 8 | baseDir: 'apps/noopen', 9 | }); 10 | return app.ready(); 11 | }); 12 | 13 | after(() => app.close()); 14 | 15 | afterEach(mm.restore); 16 | 17 | it('should return default download noopen http header', () => { 18 | return app.httpRequest() 19 | .get('/') 20 | .set('accept', 'text/html') 21 | .expect('X-Download-Options', 'noopen') 22 | .expect(200); 23 | }); 24 | 25 | it('should not return download noopen http header', async () => { 26 | const res = await app.httpRequest() 27 | .get('/disable') 28 | .set('accept', 'text/html') 29 | .expect(200); 30 | assert.equal(res.headers['x-download-options'], undefined); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/nosniff.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/nosniff.test.ts', () => { 5 | let app: MockApplication; 6 | 7 | before(async () => { 8 | app = mm.app({ 9 | baseDir: 'apps/nosniff', 10 | }); 11 | await app.ready(); 12 | }); 13 | 14 | after(() => app.close()); 15 | 16 | afterEach(mm.restore); 17 | 18 | it('should return default no-sniff http header', async () => { 19 | await app.httpRequest() 20 | .get('/') 21 | .set('accept', 'text/html') 22 | .expect('X-Content-Type-Options', 'nosniff') 23 | .expect(200); 24 | }); 25 | 26 | it('should not return download noopen http header', async () => { 27 | await app.httpRequest() 28 | .get('/disable') 29 | .set('accept', 'text/html') 30 | .expect(res => assert(!res.headers['x-content-type-options'])) 31 | .expect(200); 32 | }); 33 | 34 | it('should disable nosniff on redirect 302', async () => { 35 | await app.httpRequest() 36 | .get('/redirect') 37 | .expect(res => assert(!res.headers['x-content-type-options'])) 38 | .expect('location', '/') 39 | .expect(302); 40 | }); 41 | 42 | it('should disable nosniff on redirect 301', () => { 43 | return app.httpRequest() 44 | .get('/redirect301') 45 | .expect(res => assert(!res.headers['x-content-type-options'])) 46 | .expect('location', '/') 47 | .expect(301); 48 | }); 49 | 50 | it('should disable nosniff on redirect 307', () => { 51 | return app.httpRequest() 52 | .get('/redirect307') 53 | .expect(res => assert(!res.headers['x-content-type-options'])) 54 | .expect('location', '/') 55 | .expect(307); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/referrer.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/referrer.test.ts', () => { 4 | let app: MockApplication; 5 | let app2: MockApplication; 6 | let app3: MockApplication; 7 | 8 | before(async () => { 9 | app = mm.app({ 10 | baseDir: 'apps/referrer', 11 | }); 12 | await app.ready(); 13 | app2 = mm.app({ 14 | baseDir: 'apps/referrer-config', 15 | }); 16 | await app2.ready(); 17 | app3 = mm.app({ 18 | baseDir: 'apps/referrer-config-compatibility', 19 | }); 20 | await app3.ready(); 21 | }); 22 | 23 | after(async () => { 24 | await app.close(); 25 | await app2.close(); 26 | await app3.close(); 27 | }); 28 | 29 | afterEach(mm.restore); 30 | 31 | it('should return default referrer-policy http header', () => { 32 | return app.httpRequest() 33 | .get('/') 34 | .set('accept', 'text/html') 35 | .expect('Referrer-Policy', 'no-referrer-when-downgrade') 36 | .expect(200); 37 | }); 38 | 39 | it('should contain Referrer-Policy header when configured', () => { 40 | return app2.httpRequest() 41 | .get('/') 42 | .set('accept', 'text/html') 43 | .expect('Referrer-Policy', 'origin') 44 | .expect(200); 45 | }); 46 | 47 | it('should throw error when Referrer-Policy settings is invalid when configured', () => { 48 | const policy = 'oorigin'; 49 | return app2.httpRequest() 50 | .get(`/referrer?policy=${policy}`) 51 | .set('accept', 'text/html') 52 | .expect(new RegExp(`"${policy}" is not available.`)) 53 | .expect(500); 54 | }); 55 | 56 | it('should keep typo refererPolicy for backward compatibility', () => { 57 | const policy = 'oorigin'; 58 | return app3.httpRequest() 59 | .get(`/referrer?policy=${policy}`) 60 | .set('accept', 'text/html') 61 | .expect(new RegExp(`"${policy}" is not available.`)) 62 | .expect(500); 63 | }); 64 | 65 | // check for fix https://github.com/eggjs/security/pull/50 66 | it('should throw error when Referrer-Policy is set to index of item in ALLOWED_POLICIES_ENUM', () => { 67 | const policy = 0; 68 | return app2.httpRequest() 69 | .get(`/referrer?policy=${policy}`) 70 | .set('accept', 'text/html') 71 | .expect(new RegExp(`"${policy}" is not available.`)) 72 | .expect(500); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/safe_redirect.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | 3 | describe('test/safe_redirect.test.ts', () => { 4 | let app: MockApplication; 5 | let app2: MockApplication; 6 | before(async () => { 7 | app = mm.app({ 8 | baseDir: 'apps/safe_redirect', 9 | }); 10 | await app.ready(); 11 | app2 = mm.app({ 12 | baseDir: 'apps/safe_redirect_noconfig', 13 | }); 14 | await app2.ready(); 15 | }); 16 | 17 | after(async () => { 18 | await app.close(); 19 | await app2.close(); 20 | }); 21 | 22 | afterEach(mm.restore); 23 | 24 | it('should redirect to / when url is in white list', async () => { 25 | await app.httpRequest() 26 | .get('/safe_redirect?goto=http://domain.com') 27 | .expect(302) 28 | .expect('location', 'http://domain.com/'); 29 | }); 30 | 31 | it('should redirect to / when white list is blank', async () => { 32 | await app2.httpRequest() 33 | .get('/safe_redirect?goto=http://domain.com') 34 | .expect(302) 35 | .expect('location', 'http://domain.com/'); 36 | 37 | await app2.httpRequest() 38 | .get('/safe_redirect?goto=http://baidu.com') 39 | .expect(302) 40 | .expect('location', 'http://baidu.com/'); 41 | }); 42 | 43 | it('should redirect to / when url is invaild', async () => { 44 | app.mm(process.env, 'NODE_ENV', 'production'); 45 | await app.httpRequest() 46 | .get('/safe_redirect?goto=http://baidu.com') 47 | .expect(302) 48 | .expect('location', '/'); 49 | 50 | await app.httpRequest() 51 | .get('/safe_redirect?goto=' + encodeURIComponent('http://domain.com.baidu.com/domain.com')) 52 | .expect(302) 53 | .expect('location', '/'); 54 | 55 | await app.httpRequest() 56 | .get('/safe_redirect?goto=https://x.yahoo.com') 57 | .expect(302) 58 | .expect('location', '/'); 59 | }); 60 | 61 | it('should redirect to / when url is baidu.com', async () => { 62 | app.mm(process.env, 'NODE_ENV', 'production'); 63 | await app.httpRequest() 64 | .get('/safe_redirect?goto=baidu.com') 65 | .expect(302) 66 | .expect('location', '/'); 67 | }); 68 | 69 | it('should redirect to not safe url throw error on not production', async () => { 70 | app.mm(process.env, 'NODE_ENV', 'dev'); 71 | await app.httpRequest() 72 | .get('/safe_redirect?goto=http://baidu.com') 73 | .expect(/redirection is prohibited./) 74 | .expect(500); 75 | }); 76 | 77 | it('should redirect path directly', async () => { 78 | await app.httpRequest() 79 | .get('/safe_redirect?goto=/') 80 | .expect(302) 81 | .expect('location', '/'); 82 | 83 | await app.httpRequest() 84 | .get('/safe_redirect?goto=/foo/bar/') 85 | .expect(302) 86 | .expect('location', '/foo/bar/'); 87 | }); 88 | 89 | describe('black and white urls', () => { 90 | const blackurls = [ 91 | '//baidu.com', 92 | '///baidu.com/', 93 | 'xxx://baidu.com', 94 | 'ftp://baidu.com/', 95 | 'http://www.baidu.com?', 96 | 'http://www.baidu.com#', 97 | 'http://www.baidu.com%3F', 98 | 'http://www.domain.com@www.baidu.com', 99 | '//www.domain.com', 100 | '////////www.domain.com', 101 | 'http://hackdomain.com', 102 | 'http://domain.com.fish.com', 103 | 'http://www.domain.com.fish.com', 104 | '', 105 | ' ', 106 | '//foo', 107 | 'http://baidu.com/123123\r\nHEADER', 108 | '', 109 | 'http:///123', 110 | ]; 111 | 112 | const whiteurls = [ 113 | 'http://domain.com/', 114 | 'http://domain.com/foo', 115 | 'http://domain.com/foo/bar?a=123', 116 | ]; 117 | 118 | it('should block', async () => { 119 | app.mm(process.env, 'NODE_ENV', 'production'); 120 | for (const url of blackurls) { 121 | await app.httpRequest() 122 | .get('/safe_redirect?goto=' + encodeURIComponent(url)) 123 | .expect('location', '/') 124 | .expect(302); 125 | } 126 | }); 127 | 128 | it('should block evil path', async () => { 129 | app.mm(process.env, 'NODE_ENV', 'production'); 130 | 131 | await app.httpRequest() 132 | .get('/safe_redirect?goto=' + encodeURIComponent('/\\evil.com/')) 133 | .expect('location', '/') 134 | .expect(302); 135 | }); 136 | 137 | it('should block illegal url', async () => { 138 | app.mm(process.env, 'NODE_ENV', 'production'); 139 | await app.httpRequest() 140 | .get('/safe_redirect?goto=' + encodeURIComponent('http://domain.com%0a.cn/path?abc=bar#123')) 141 | .expect(302) 142 | .expect('location', '/'); 143 | }); 144 | 145 | it('should block evil url', async () => { 146 | app.mm(process.env, 'NODE_ENV', 'production'); 147 | await app.httpRequest() 148 | .get('/safe_redirect?goto=' + encodeURIComponent('http://domain.com!.a.cn/path?abc=bar#123')) 149 | .expect(302) 150 | .expect('location', '/'); 151 | }); 152 | 153 | it('should pass', async () => { 154 | for (const url of whiteurls) { 155 | await app.httpRequest() 156 | .get('/safe_redirect?goto=' + encodeURIComponent(url)) 157 | .expect('location', url) 158 | .expect(302); 159 | } 160 | }); 161 | }); 162 | 163 | describe('unsafeRedirect()', () => { 164 | it('should redirect to unsafe url', async () => { 165 | const urls = [ 166 | 'http://baidu.com/', 167 | 'http://xxx.oo.com/123.html', 168 | ]; 169 | for (const url of urls) { 170 | await app.httpRequest() 171 | .get('/unsafe_redirect?goto=' + encodeURIComponent(url)) 172 | .expect(302) 173 | .expect('location', url); 174 | } 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/security.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/security.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | let app3: MockApplication; 8 | let app4: MockApplication; 9 | before(async () => { 10 | app = mm.app({ 11 | baseDir: 'apps/security', 12 | }); 13 | await app.ready(); 14 | app2 = mm.app({ 15 | baseDir: 'apps/security-unset', 16 | }); 17 | await app2.ready(); 18 | app3 = mm.app({ 19 | baseDir: 'apps/security-override-controller', 20 | }); 21 | await app3.ready(); 22 | app4 = mm.app({ 23 | baseDir: 'apps/security-override-middleware', 24 | }); 25 | await app4.ready(); 26 | }); 27 | 28 | after(async () => { 29 | await app.close(); 30 | await app2.close(); 31 | await app3.close(); 32 | await app4.close(); 33 | }); 34 | 35 | afterEach(mm.restore); 36 | 37 | it('should load default security headers', () => { 38 | return app.httpRequest() 39 | .get('/') 40 | .set('accept', 'text/html') 41 | .expect('X-Download-Options', 'noopen') 42 | .expect('X-Content-Type-Options', 'nosniff') 43 | .expect('X-XSS-Protection', '1; mode=block') 44 | .expect(200); 45 | }); 46 | 47 | it('should load default security headers when developer try to override in controller', () => { 48 | return app3.httpRequest() 49 | .get('/') 50 | .set('accept', 'text/html') 51 | .expect('Strict-Transport-Security', 'max-age=31536000') 52 | .expect('X-Download-Options', 'noopen') 53 | .expect('X-Content-Type-Options', 'nosniff') 54 | .expect('X-XSS-Protection', '1; mode=block') 55 | .expect(200); 56 | }); 57 | 58 | it('should load default security headers when developer try to override in middleware', async () => { 59 | const res = await app4.httpRequest() 60 | .get('/') 61 | .set('accept', 'text/html') 62 | .expect('Strict-Transport-Security', 'max-age=31536000') 63 | .expect('X-Download-Options', 'noopen') 64 | .expect('X-Content-Type-Options', 'nosniff') 65 | .expect('X-XSS-Protection', '1; mode=block') 66 | .expect(200); 67 | assert.equal(res.status, 200); 68 | }); 69 | 70 | it('disable hsts for default', async () => { 71 | const res = await app2.httpRequest() 72 | .get('/') 73 | .set('accept', 'text/html'); 74 | assert.equal(res.headers['strict-transport-security'], undefined); 75 | }); 76 | 77 | it('should not load security headers when set to enable:false', async () => { 78 | const res = await app2.httpRequest() 79 | .get('/') 80 | .set('accept', 'text/html'); 81 | assert.equal(res.headers['X-Frame-Options'], undefined); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/xframe.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { mm, MockApplication } from '@eggjs/mock'; 3 | 4 | describe('test/xframe.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | let app3: MockApplication; 8 | let app4: MockApplication; 9 | 10 | before(async () => { 11 | app = mm.app({ 12 | baseDir: 'apps/iframe', 13 | }); 14 | await app.ready(); 15 | 16 | app2 = mm.app({ 17 | baseDir: 'apps/iframe-novalue', 18 | }); 19 | await app2.ready(); 20 | 21 | app3 = mm.app({ 22 | baseDir: 'apps/iframe-allowfrom', 23 | }); 24 | await app3.ready(); 25 | 26 | app4 = mm.app({ 27 | baseDir: 'apps/iframe-black-urls', 28 | }); 29 | await app4.ready(); 30 | }); 31 | 32 | after(async () => { 33 | await app.close(); 34 | await app2.close(); 35 | await app3.close(); 36 | await app4.close(); 37 | }); 38 | 39 | afterEach(mm.restore); 40 | 41 | it('should contain X-Frame-Options: SAMEORIGIN', async () => { 42 | await app.httpRequest() 43 | .get('/') 44 | .set('accept', 'text/html') 45 | .expect('X-Frame-Options', 'SAMEORIGIN'); 46 | 47 | await app.httpRequest() 48 | .get('/foo') 49 | .set('accept', 'text/html') 50 | .expect('X-Frame-Options', 'SAMEORIGIN'); 51 | }); 52 | 53 | it('should contain X-Frame-Options: ALLOW-FROM http://www.domain.com by this.securityOptions', async () => { 54 | const res = await app.httpRequest() 55 | .get('/options') 56 | .set('accept', 'text/html'); 57 | assert.equal(res.status, 200); 58 | assert.equal(res.headers['x-frame-options'], 'ALLOW-FROM http://www.domain.com'); 59 | }); 60 | 61 | it('should contain X-Frame-Options: SAMEORIGIN when dont set value option', function(done) { 62 | app2.httpRequest() 63 | .get('/foo') 64 | .set('accept', 'text/html') 65 | .expect('X-Frame-Options', 'SAMEORIGIN', done); 66 | }); 67 | 68 | it('should contain X-Frame-Options: ALLOW-FROM with page when set ALLOW-FROM and page option', function(done) { 69 | app3.httpRequest() 70 | .get('/foo') 71 | .set('accept', 'text/html') 72 | .expect('X-Frame-Options', 'ALLOW-FROM http://www.domain.com', done); 73 | }); 74 | 75 | it('should not contain X-Frame-Options: SAMEORIGIN when use ignore', async () => { 76 | let res = await app.httpRequest() 77 | .get('/hello') 78 | .set('accept', 'text/html') 79 | .expect(200); 80 | assert.equal(res.headers['X-Frame-Options'], undefined); 81 | 82 | res = await app4.httpRequest() 83 | .get('/hello') 84 | .set('accept', 'text/html') 85 | .expect(200); 86 | assert.equal(res.headers['X-Frame-Options'], undefined); 87 | 88 | res = await app.httpRequest() 89 | .get('/world/12') 90 | .set('accept', 'text/html') 91 | .expect(200); 92 | assert.equal(res.headers['X-Frame-Options'], undefined); 93 | 94 | res = await app.httpRequest() 95 | .get('/world/12?xx=xx') 96 | .set('accept', 'text/html') 97 | .expect(200); 98 | assert.equal(res.headers['X-Frame-Options'], undefined); 99 | 100 | res = await app2.httpRequest() 101 | .get('/hello') 102 | .set('accept', 'text/html') 103 | .expect(200); 104 | assert.equal(res.headers['X-Frame-Options'], undefined); 105 | 106 | res = await app2.httpRequest() 107 | .get('/world/12') 108 | .set('accept', 'text/html') 109 | .expect(200); 110 | assert.equal(res.headers['X-Frame-Options'], undefined); 111 | 112 | res = await app2.httpRequest() 113 | .get('/world/12?xx=xx') 114 | .set('accept', 'text/html') 115 | .expect(200); 116 | assert.equal(res.headers['X-Frame-Options'], undefined); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/xss.test.ts: -------------------------------------------------------------------------------- 1 | import { mm, MockApplication } from '@eggjs/mock'; 2 | import snapshot from 'snap-shot-it'; 3 | 4 | describe('test/xss.test.ts', () => { 5 | let app: MockApplication; 6 | let app2: MockApplication; 7 | let app3: MockApplication; 8 | 9 | before(async () => { 10 | app = mm.app({ 11 | baseDir: 'apps/xss', 12 | }); 13 | await app.ready(); 14 | 15 | app2 = mm.app({ 16 | baseDir: 'apps/xss-close', 17 | }); 18 | await app2.ready(); 19 | 20 | app3 = mm.app({ 21 | baseDir: 'apps/xss-close-zero', 22 | }); 23 | await app3.ready(); 24 | }); 25 | 26 | after(async () => { 27 | await app.close(); 28 | await app2.close(); 29 | await app3.close(); 30 | }); 31 | 32 | afterEach(mm.restore); 33 | 34 | it('should contain default X-XSS-Protection header', () => { 35 | return app.httpRequest() 36 | .get('/') 37 | .set('accept', 'text/html') 38 | .expect('X-XSS-Protection', '1; mode=block') 39 | .expect(200); 40 | }); 41 | 42 | it('should set X-XSS-Protection header value 0 by this.securityOptions', () => { 43 | return app.httpRequest() 44 | .get('/0') 45 | .set('accept', 'text/html') 46 | .expect('X-XSS-Protection', '0') 47 | .expect(200); 48 | }); 49 | 50 | it('should set X-XSS-Protection header value 0', () => { 51 | return app2.httpRequest() 52 | .get('/') 53 | .set('accept', 'text/html') 54 | .expect('X-XSS-Protection', '0') 55 | .expect(200); 56 | }); 57 | 58 | it('should set X-XSS-Protection header value 0 when config is number 0', () => { 59 | snapshot(app3.config.security.xssProtection); 60 | return app3.httpRequest() 61 | .get('/') 62 | .set('accept', 'text/html') 63 | .expect('X-XSS-Protection', '0') 64 | .expect(200); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | --------------------------------------------------------------------------------