├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── assets ├── images │ ├── NodeSecurityShield-Full.png │ └── NodeSecurityShield.png └── screenshots │ ├── Sentry.png │ └── Sentry1.png ├── index.js ├── lib ├── attackMonitoring.js └── hook.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directories 23 | node_modules/ 24 | jspm_packages/ 25 | 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Microbundle cache 34 | .rpt2_cache/ 35 | .rts2_cache_cjs/ 36 | .rts2_cache_es/ 37 | .rts2_cache_umd/ 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Output of 'npm pack' 43 | *.tgz 44 | 45 | # Yarn Integrity file 46 | .yarn-integrity 47 | 48 | # dotenv environment variables file 49 | .env 50 | .env.test 51 | 52 | # assets for github 53 | assets 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Node Security Shield 5 | 6 | **Node Security Shield (NSS)** is an Open source Runtime Application Self-Protection (**RASP**) tool which aims at bridging the gap for comprehensive NodeJS security by enabling *Developer* and *Security Engineer* to declare what resources an application can access. 7 | 8 | Inspired by the Log4Shell ([CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228)) vulnerability which can be exploited because an application can make arbitrary network calls, we felt there is a need for an application to have a mechanism so that it can declare what privileges it allows in order to make the exploitation of such vulnerabilities harder by implementing additional controls. 9 | 10 | In order to achieve this, **NSS (Node Security Shield)** has **Resource Access Policy (RAP)** 11 | 12 | ### Resource Access Policy (RAP) 13 | 14 | **Resource Access Policy** is similar to **CSP**([Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)). 15 | 16 | It lets the developer/security engineer declare what resources an application should access. And **Node Security Shield** will enforce it. 17 | 18 | ## Installation 19 | 20 | ### Install *NodeSecurityShield* using npm 21 | 22 | ```bash 23 | npm install nodesecurityshield 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```jsx 29 | // Require Node Security Shield 30 | let nodeSecurityShield = require('nodesecurityshield'); 31 | 32 | // Enable Attack Monitoring and/or Blocking 33 | nodeSecurityShield.enableAttackMonitoring("Unique-App-Id",resourceAccessPolicy ,callbackFunction); 34 | ``` 35 | 36 | ### **Sample *resourceAccessPolicy [BASIC]*** 37 | 38 | ```jsx 39 | const resourceAccessPolicy = { 40 | "outBoundRequest" : { 41 | 42 | "blockedDomains" : ["compromised.domdog.io"], 43 | 44 | "allowedDomains" : [] 45 | }, 46 | "executedCommand": { 47 | 48 | "allowedCommands": ["pwd" , "(node)[ ](helper\/)[a-z|0-9]*(.js)"] 49 | } 50 | }; 51 | ``` 52 | 53 | - **`outBoundRequest`:** defines the accepted behaviour for `Outbound Requests` 54 | - **Note:** blockedDomains hold precedence over allowedDomains. 55 | - **i.e.,** requests checked against blockedDomains first then allowedDomains. 56 | - **`executedCommand`:** defines the accepted behaviour for `Command Execution` 57 | - `allowedCommands` Array accepts `String` . You can pass a RegEx to allow a pattern of commands. 58 | - The default behaviour is to block all command executions. Provided, RAP has `executedCommand` property defined. 59 | - When the above RAP is used, 60 | - `pwd` command is allowed to execute and 61 | - `.js` files inside `helper` are allowed to spawn as node processes. 62 | - **Note:** Avoid usage of `.*` in regex. As this will allow the execution of any command after a pipe `|`. 63 | 64 | ### **Sample *callbackFunction* to log RAP violations to console.** 65 | 66 | ```jsx 67 | var callbackFunction = function (violationEvent,violations,violationLimitPerMinReached) { 68 | console.log(JSON.stringify(violationEvent,null, 4)); 69 | } 70 | ``` 71 | 72 | - ***`violationEvent` -** RAP violation which occurred. It is presented as a CSP violation.* 73 | - ***`violations` -** RAP violation count. Resets to ZERO every minute.* 74 | - ***`violationLimitPerMinReached` -** true if RAP violations count exceeds 'maxViolationsPerMinute’ [ an option in RAP]* 75 | - **To Block an Attack** - throw an error 76 | 77 | ```jsx 78 | throw new Error("Request Blocked. It violates declared Resource Access Policy.") 79 | ``` 80 | 81 | 82 | ### **Sample violationEvent** 83 | 84 | ```jsx 85 | { 86 | "csp-report": { 87 | "document-uri": "https://Unique-App-Id", 88 | "blocked-uri": "https://compromised.domdog.io:443", 89 | "violated-directive": "connect-src", 90 | "effective-directive": "connect-src", 91 | "original-policy": "{\"outBoundRequest\":{\"blockedDomains\":[\"compromised.domdog.io\"],\"allowedDomains\":[]}}", 92 | "disposition": "report", 93 | "status-code": 200, 94 | "script-sample": "", 95 | "source-file": "Error\n at TLSSocket.obj. [as connect] (/mnt/c/Ironwasp/Product/NodeSecurityShield/lib/hook.js:20:25)\n at Object.connect (_tls_wrap.js:1606:13)\n at Agent.createConnection (https.js:126:22)\n at Agent.createSocket (_http_agent.js:273:26)\n at Agent.addRequest (_http_agent.js:232:10)\n at new ClientRequest (_http_client.js:302:16)\n at request (https.js:310:10)\n at Object.get (https.js:314:15)\n at /mnt/c/Ironwasp/RD/Node/SimpleVulnerableNode/routes/ssrf.js:19:19\n at Layer.handle [as handle_request] (/mnt/c/Ironwasp/RD/Node/SimpleVulnerableNode/node_modules/express/lib/router/layer.js:95:5)" 96 | } 97 | } 98 | ``` 99 | 100 | - **`document-uri:`** contains Unique-App-Id, passed during initialization of NSS 101 | - **`blocked-uri`:** domain of the outbound request which violated RAP 102 | - **`violated-directive`:** `connect-src` is a synonym for `Outbound Request` likewise `script-src` is a synonym for `Command Execution` 103 | - **`original-policy`:** violated Resource Access Policy (RAP) 104 | - **`source-file`:** Stack Trace of where this violation. 105 | 106 | ## Integrating with Sentry 107 | 108 | ### **Sample *resourceAccessPolicy to integrate with [Sentry](https://sentry.io/)*** 109 | 110 | ```jsx 111 | const resourceAccessPolicy = { 112 | "outBoundRequest" : { 113 | 114 | "blockedDomains" : ["compromised.domdog.io"], 115 | 116 | "allowedDomains" : [] 117 | }, 118 | "executedCommand": { 119 | 120 | "allowedCommands": ["pwd" , "(node)[ ](helper\/)[a-z|0-9]*(.js)"] 121 | }, 122 | "reportUri": "https://ingest.sentry.io/api/6011856/security/?sentry_key=", 123 | 124 | }; 125 | ``` 126 | 127 | - **`outBoundRequest`:** defines the accepted behaviour for `Outbound Requests` 128 | - **Note:** blockedDomains hold precedence over allowedDomains. 129 | - **i.e.,** requests checked against blockedDomains first then allowedDomains. 130 | - **`executedCommand`:** defines the accepted behaviour for `Command Execution` 131 | - `allowedCommands` Array accepts `String` . You can pass a RegEx to allow a pattern of commands. 132 | - The default behaviour is to block all command executions. Provided, RAP has `executedCommand` property defined. 133 | - When the above RAP is used, 134 | - `pwd` command is allowed to execute and 135 | - `.js` files inside `helper` are allowed to spawn as node processes. 136 | - **Note:** Avoid usage of `.*` in regex. As this will allow the execution of any command after a pipe `|`. 137 | - **`reportUri` :** Sends Violations to a given endpoint. As violations are similar to Content Security Policy violations. Any CSP monitoring solutions can be used. We used the Sentry endpoint in the above RAP. 138 | 139 | **Screenshot from Sentry dashboard** 140 | ![sentry issues](/assets/screenshots/Sentry1.png) 141 | ![sentry issues](/assets/screenshots/Sentry.png) 142 | 143 | ### **Sample *resourceAccessPolicy [Advanced]*** 144 | 145 | ```jsx 146 | const resourceAccessPolicy = { 147 | "outBoundRequest" : { 148 | 149 | "blockedDomains" : ["compromised.domdog.io"], 150 | 151 | "allowedDomains" : ["domdog.io","*.domdog.io", 152 | 153 | { 154 | "domains": [ 155 | "domgo.at", 156 | ], 157 | "modules": [ 158 | { 159 | "file": "\/routes\/ssrf.js", 160 | }, 161 | { 162 | "file": "\/node_modules\/axios\/", 163 | } 164 | ] 165 | }, 166 | { 167 | "domains": [ 168 | "cluster0-shard-00-00.lb9jm.mongodb.net", 169 | "cluster0-shard-00-01.lb9jm.mongodb.net", 170 | "cluster0-shard-00-02.lb9jm.mongodb.net" 171 | ], 172 | "modules": [ 173 | { 174 | "file": "\/node_modules\/mongodb\/" 175 | } 176 | ] 177 | } 178 | 179 | ] 180 | }, 181 | "executedCommand": { 182 | 183 | "allowedCommands": ["pwd" , "(node)[ ](helper\/)[a-z|0-9]*(.js)"] 184 | }, 185 | 186 | "reportUri": "https://endpoint-to-send-violations", 187 | 188 | "maxViolationsPerMinute": 50 189 | } 190 | ``` 191 | 192 | - **`outBoundRequest`:** defines the accepted behaviour for `Outbound Requests` 193 | - **Note:** blockedDomains hold precedence over allowedDomains. 194 | - **i.e.,** requests checked against blockedDomains first then allowedDomains. 195 | - **`executedCommand`:** defines the accepted behaviour for `Command Execution` 196 | - `allowedCommands` Array accepts `String` . You can pass a RegEx to allow a pattern of commands. 197 | - The default behaviour is to block all command executions. Provided, RAP has `executedCommand` property defined. 198 | - When the above RAP is used, 199 | - `pwd` command is allowed to execute and 200 | - `.js` files inside `helper` are allowed to spawn as node processes. 201 | - **Note:** Avoid usage of `.*` in regex. As this will allow the execution of any command after a pipe `|`. 202 | - **`reportUri`:** Sends Violations to a given endpoint. As violations are similar to **Content Security Policy** violations. Any CSP monitoring solutions can be used. We used the Sentry endpoint in the above RAP. 203 | - **Module Specific Control:** `allowedDomain` Array accepts Objects with following 204 | - `domain`: Array of domains which are to be allowed for provided files. 205 | - `modules`: Array of Objects containing file paths. Only outbound Requests made through these files to specified domains are allowed. 206 | - **`maxViolationsPerMinute`:** Maximum number of violations to be sent to the `reportUri`. 207 | If not specified, the default value (*100 violations*) is used. 208 | 209 | ## Features 210 | 211 | - **Attack Monitoring** 212 | - Outbound Network Calls 213 | - Command Execution 214 | - **Attack Blocking** 215 | - Outbound Network Calls 216 | - Command Execution 217 | - **Module Specific Control** 218 | 219 | ## Roadmap 220 | 221 | - **Attack Monitoring** 222 | - File Calls 223 | - **Attack Blocking** 224 | - File Calls 225 | - **Vulnerability Scanner** 226 | 227 | ## Authors 228 | 229 | - Lavakumar Kuppan 230 | - Github - [@lavakumar](https://github.com/Lavakumar) 231 | - Twitter - [@lavakumark](https://twitter.com/lavakumark) 232 | - Sukesh Pappu 233 | - Github - [@thelogicalbeard](https://www.github.com/thelogicalbeard) 234 | - Twitter - [@thelogicalbeard](https://www.twitter.com/thelogicalbeard) 235 | 236 | 237 | 238 | 239 | ## Contributors 240 | 241 | - Ayusman Samal 242 | - Github - [@p1xxxel](https://github.com/p1xxxel) 243 | 244 | 245 | 246 | ## License 247 | 248 | [Apache License 2.0](/LICENSE) 249 | 250 | -------------------------------------------------------------------------------- /assets/images/NodeSecurityShield-Full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomdogSec/NodeSecurityShield/27663a6648f80c28747ac9b87f6cdc651c8d988b/assets/images/NodeSecurityShield-Full.png -------------------------------------------------------------------------------- /assets/images/NodeSecurityShield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomdogSec/NodeSecurityShield/27663a6648f80c28747ac9b87f6cdc651c8d988b/assets/images/NodeSecurityShield.png -------------------------------------------------------------------------------- /assets/screenshots/Sentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomdogSec/NodeSecurityShield/27663a6648f80c28747ac9b87f6cdc651c8d988b/assets/screenshots/Sentry.png -------------------------------------------------------------------------------- /assets/screenshots/Sentry1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomdogSec/NodeSecurityShield/27663a6648f80c28747ac9b87f6cdc651c8d988b/assets/screenshots/Sentry1.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./lib/hook').initHooks(); 2 | module.exports.enableAttackMonitoring = require("./lib/attackMonitoring.js").enableAttackMonitoring; -------------------------------------------------------------------------------- /lib/attackMonitoring.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | 4 | // Make local copy of JSON.stringify and setTimeout 5 | const stringify = JSON.stringify; 6 | const timeoutSet = setTimeout; 7 | 8 | //default state is false. 9 | let allowOutboundRequest = false; 10 | let allowCommand = false; 11 | let policy; 12 | let cbFunction; 13 | let app; 14 | let attackMonitoring = false; // Default, not enabled. 15 | var violations = 0; 16 | //default violationLimitPerMin is 100. 17 | var violationLimitPerMin = 100; 18 | //default is false, is true when violations > violationLimitPerMin 19 | var violationLimitPerMinReached = false; 20 | var nssVersion; 21 | var reportUri; 22 | var reportUriDomain; 23 | var reportUriIsHttp = false; 24 | 25 | function isPureObject(input) { 26 | return null !== input && typeof input === 'object' && Object.getPrototypeOf(input).isPrototypeOf(Object); 27 | } 28 | 29 | function isString(input) { 30 | return typeof input === 'string'; 31 | } 32 | 33 | function wildcardCheck(wildcardDomain, outBoundReqDomain) { 34 | wildcardDomain = wildcardDomain.split('*').pop(); 35 | if (outBoundReqDomain.length > wildcardDomain.length) { 36 | if (outBoundReqDomain.substr(outBoundReqDomain.length - wildcardDomain.length) == wildcardDomain) { 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | function extractHostname(url) { 44 | var hostname; 45 | //find & remove protocol (http, ftp, etc.) and get hostname 46 | 47 | if (url.indexOf("//") > -1) { 48 | hostname = url.split('/')[2]; 49 | } else { 50 | hostname = url.split('/')[0]; 51 | } 52 | 53 | //find & remove port number 54 | hostname = hostname.split(':')[0]; 55 | //find & remove "?" 56 | hostname = hostname.split('?')[0]; 57 | 58 | return hostname.toLowerCase(); 59 | } 60 | 61 | function checkOutboundRequest(outBoundReqDomain, stack, blockedArray, allowedArray, allowedModuleArray){ 62 | //blockedWildcardDomains => array of domains where all sub domains are blocked. (*) 63 | const blockedWildcardDomains = blockedArray.filter(domain => domain[0]+domain[1] == "*."); 64 | //blockedDomains => array of domains to be blocked. (without wildcard) 65 | const blockedDomains = blockedArray.filter(domain => domain[0] !== "*"); 66 | //allowedWildcardDomains => array of domains where sub domains are allowd. (*) 67 | const allowedWildcardDomains = allowedArray.filter(domain => domain[0]+domain[1] == "*."); 68 | //allowedDomains => array of domains which are allowed. (without wildcard) 69 | const allowedDomains = allowedArray.filter(domain => domain[0] !== "*"); 70 | 71 | outBoundReqDomain = outBoundReqDomain.toLowerCase(); 72 | 73 | /** 74 | * Allow outbound requests to reportUri 75 | */ 76 | if (outBoundReqDomain === reportUriDomain) { 77 | return true 78 | } 79 | 80 | if (blockedArray.length > 0) { 81 | //check outbound request in blockedDomains (without wildcard) 82 | if (blockedDomains.includes(outBoundReqDomain)) { 83 | //Outbound Request not allowed 84 | 85 | return false; 86 | } else {//check outbound request in blockedWildcardDomains 87 | for (i = 0; i < blockedWildcardDomains.length; i++) { 88 | if (wildcardCheck(blockedWildcardDomains[i], outBoundReqDomain)){ 89 | //Outbound Request not allowed 90 | return false; 91 | } 92 | } 93 | } 94 | } 95 | 96 | if (allowedArray.length + allowedModuleArray.length === 0) { 97 | return true 98 | } 99 | 100 | if (allowedArray.length > 0) { 101 | //check outbound request in allowedDomains (without wildcard) 102 | 103 | if (allowedDomains.includes(outBoundReqDomain)) { 104 | //Outbound Request allowed 105 | return true; 106 | } else { // check outbound request in allowedWildcardDomains 107 | for (i = 0; i < allowedWildcardDomains.length; i++) { 108 | if(wildcardCheck(allowedWildcardDomains[i], outBoundReqDomain)){ 109 | //Outbound Request allowed 110 | return true; 111 | } 112 | } 113 | } 114 | } 115 | 116 | if (allowedModuleArray.length > 0) { 117 | //check outbound request and module in allowedModuleArray 118 | let fileLines; 119 | const lines = stack.split('\n').slice(1); 120 | fileLines = lines.map(function(line){ 121 | const lineMatch = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/); 122 | const fileName = lineMatch[2]; 123 | return fileName; 124 | }).join('\n'); 125 | for (i = 0; i < allowedModuleArray.length; i++) { 126 | let allowedModuleDomains = allowedModuleArray[i].domains.filter(domain => domain[0] != "*"); 127 | let allowedModuleWildcardDomains = allowedModuleArray[i].domains.filter(domain => domain[0]+domain[1] == "*."); 128 | let modulePaths = allowedModuleArray[i].modules.map(module => module.file); 129 | if (allowedModuleDomains.includes(outBoundReqDomain)){ 130 | for (j=0; j < modulePaths.length; j++){ 131 | const regex = new RegExp(modulePaths[j]); 132 | if(regex.test(fileLines)){ 133 | //Outbound Request allowed 134 | return true; 135 | } 136 | } 137 | }else { 138 | for (j = 0; j < allowedModuleWildcardDomains.length; j++) { 139 | if (wildcardCheck(allowedModuleWildcardDomains[j], outBoundReqDomain)){ 140 | for (k=0; k < modulePaths.length; k++) { 141 | const regex = new RegExp(modulePaths[k]); 142 | if(regex.test(fileLines)){ 143 | //Outbound Request allowed 144 | return true; 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | //default state is to false. 154 | return false; 155 | } 156 | 157 | function checkExecutedCommand(command, allowedCommands) { 158 | for (i = 0; i < allowedCommands.length; i++) { 159 | const regex = new RegExp(allowedCommands[i]); 160 | if(regex.test(command)) { 161 | return true; 162 | } 163 | } 164 | return false; 165 | } 166 | 167 | /** 168 | * callbackFunction for handling RAP violations 169 | * 170 | * @callback callbackFunction 171 | * @param {JSON} violationEvent RAP violation which occurred. It is presented as a CSP violation. 172 | * @param {Number} violations RAP violation count. Resets to ZERO every minute. 173 | * @param {Boolean} violationLimitPerMinReached true if RAP violations count exceeds 'maxViolationsPerMinute' 174 | * 175 | */ 176 | 177 | /** 178 | * For outBoundRequest , blockedDomains have greater presidence over allowedDomains. 179 | * i.e., requests are checked against blockedDomains first then allowedDoamins. 180 | * 181 | * @param {String} appId A unique identifier for this instance of NSS 182 | * @param {Object} policyJSON Resource Access Policy as a JSON 183 | * @param {callbackFunction} callbackFunction - callbackFunction that handles RAP violations 184 | */ 185 | let enableAttackMonitoring = function (appId, policyJSON, callbackFunction) { 186 | /** 187 | * Set appId. Changing it requires a server/application restart. 188 | */ 189 | if (typeof app === 'undefined') { 190 | app = appId; 191 | } 192 | 193 | /** 194 | * Set Resource Access Policy. Changing it requires a server/application restart. 195 | */ 196 | if (typeof policy === 'undefined') { 197 | policy = policyJSON; 198 | } 199 | 200 | /** 201 | * Set callbackFunction. Changing it requires a server/application restart. 202 | */ 203 | if (typeof cbFunction === 'undefined') { 204 | cbFunction = callbackFunction; 205 | } 206 | 207 | /** 208 | * If attackMonitoring not enabled. 209 | */ 210 | if (!attackMonitoring) { 211 | 212 | /** 213 | * Check for 'outBoundRequest' in RAP. 214 | * If exists and contains entries, 215 | * then enable Attack Monitoring for all outbound requests. 216 | */ 217 | 218 | // check and set reportUri 219 | if (policyJSON.hasOwnProperty('reportUri')){ 220 | if (isString(policyJSON.reportUri)){ 221 | reportUri = policyJSON.reportUri; 222 | reportUriDomain = extractHostname(reportUri); 223 | if (reportUri.toLowerCase().startsWith("http:")) { // its http 224 | reportUriIsHttp = true; 225 | } 226 | 227 | // check and set the violationLimitPerMin 228 | if (policyJSON.hasOwnProperty('maxViolationsPerMinute')){ 229 | if (typeof policyJSON['maxViolationsPerMinute'] === 'number') { 230 | violationLimitPerMin = policyJSON['maxViolationsPerMinute']; 231 | }else{ 232 | // todo: log - RAP's maxViolationsPerMinute value is expected to be a number 233 | } 234 | } 235 | 236 | // Reset the violations count every minute. 237 | setInterval(violationReset, 60000); 238 | }else{ 239 | // todo: log - RAP's reportUri value is expected to be a string 240 | } 241 | } 242 | 243 | // Check if any valid property for attackMonitoring is present in policyJSON. 244 | if (policyJSON.hasOwnProperty('outBoundRequest') || policyJSON.hasOwnProperty('executedCommand')) { 245 | 246 | var totalLength = 0; 247 | if ("outBoundRequest" in policyJSON) { 248 | //todo: enable logging for policy file related exceptions 249 | const blockedArray = policyJSON.outBoundRequest.blockedDomains.map(e => e.trim()).map(e => e.toLowerCase()); 250 | const allowedArray = policyJSON.outBoundRequest.allowedDomains.filter(domain => isString(domain)).map(e => e.trim()).map(e => e.toLowerCase()); 251 | const allowedModuleArray = policyJSON.outBoundRequest.allowedDomains.filter(domain => isPureObject(domain)).map(function(obj){ 252 | obj.domains = obj.domains.map(e => e.toLowerCase()); 253 | return obj;}); 254 | 255 | if ((blockedArray.length + allowedArray.length + allowedModuleArray.length) > 0) { 256 | totalLength += blockedArray.length + allowedArray.length + allowedModuleArray.length; 257 | allowOutboundRequest = (outBoundReqDomain, stack) => { 258 | // Check outBoundReqDomain. 259 | return checkOutboundRequest(outBoundReqDomain, stack, blockedArray, allowedArray, allowedModuleArray); 260 | } 261 | 262 | } 263 | 264 | } 265 | 266 | if ("executedCommand" in policyJSON) { 267 | const allowedCommandsArray = policyJSON.executedCommand.allowedCommands.map(e => e.trim()).map(e => e.toLowerCase()); 268 | 269 | if (allowedCommandsArray.length == 0) { 270 | allowCommand = (executedCommand) => { 271 | // Block all executedCommand. 272 | return false; 273 | } 274 | }else if (allowedCommandsArray.length > 0) { 275 | totalLength += allowedCommandsArray.length; 276 | allowCommand = (executedCommand) => { 277 | // Check executedCommand. 278 | return checkExecutedCommand(executedCommand, allowedCommandsArray); 279 | } 280 | } 281 | } 282 | 283 | // attackMonitoring is enabled only if there are any valid entries 284 | if (totalLength > 0) { 285 | //Enable attackMonitoring 286 | attackMonitoring = true; 287 | console.log('NSS : Attack Monitoring enabled. '); 288 | 289 | //Get NodeSecurityShield Version 290 | nssVersion = require('../package.json').version; 291 | } 292 | 293 | } 294 | 295 | 296 | 297 | 298 | } 299 | 300 | } 301 | 302 | // Send CSP Report. 303 | function sendReport(violationEvent){ 304 | const url = reportUri; 305 | const data = stringify(violationEvent); 306 | const options = { 307 | method: 'POST', 308 | headers: { 309 | 'Content-Type': 'application/csp-report', 310 | 'Content-Length': data.length, 311 | 'User-Agent': "Node Security Shield/" + nssVersion, 312 | }, 313 | }; 314 | // Check if reportUri is http or https. 315 | if (reportUriIsHttp) { 316 | const req = http.request(url, options); 317 | req.write(data); 318 | req.end(); 319 | } else { 320 | const req = https.request(url, options); 321 | req.write(data); 322 | req.end(); 323 | } 324 | } 325 | 326 | // Reset violations 327 | function violationReset() { 328 | violations = 0; 329 | } 330 | 331 | // Prepare CSP Report according to eventType. 332 | function prepareReport(args, eventType, stack) { 333 | let cspReport = {}; 334 | let blockedParam; 335 | let directive; 336 | let scriptSample = ""; 337 | switch (eventType) { 338 | case 'socket': 339 | directive = "connect-src"; 340 | blockedParam = args.protocol + "//" + args.host + ":" + args.port; 341 | break; 342 | case 'command': 343 | directive = "script-src"; 344 | blockedParam = "https://command-execution"; 345 | scriptSample = args.join(' '); 346 | break; 347 | } 348 | cspReport["document-uri"] = "https://"+app; 349 | cspReport["blocked-uri"] = blockedParam; 350 | cspReport["violated-directive"] = directive; 351 | cspReport["effective-directive"] = directive; 352 | cspReport["original-policy"] = stringify(policy); 353 | cspReport["disposition"] = "report"; 354 | cspReport["status-code"] = 200; 355 | cspReport["script-sample"] = scriptSample; 356 | cspReport["source-file"] = stack; 357 | return cspReport; 358 | } 359 | 360 | let checkSocketConnection = function (args, stack) { 361 | 362 | let arg0; 363 | if (Array.isArray(args[0])) { //true if HTTP 364 | arg0 = args[0][0]; 365 | } else { 366 | arg0 = args[0]; 367 | } 368 | if (typeof arg0 === 'object' && arg0 !== null) { 369 | if (('port' in arg0) && ('host' in arg0)) { // TCP Connection. 370 | if (!allowOutboundRequest(arg0.host, stack)) { 371 | violations++; 372 | let violationEvent = {}; 373 | violationEvent["csp-report"] = prepareReport(arg0, "socket", stack); 374 | 375 | // If reportUri has been set and limit has not been reached, send csp report. 376 | if ( typeof reportUri === 'string') { 377 | if (violationLimitPerMin - violations >= 0) { 378 | timeoutSet(sendReport, 10, violationEvent); 379 | } 380 | } 381 | 382 | cbFunction(violationEvent, violations, violationLimitPerMinReached); 383 | } 384 | } else { 385 | //todo: log if there is no port or no host in arguments. 386 | //console.log(args) 387 | //console.log("Host and Port are not part of the argument passed to net.socket.connect") 388 | } 389 | } 390 | } 391 | 392 | let checkCommandExecution = function(args, stack) { 393 | let arg0; 394 | arg0 = args['0'].args; 395 | var executedCommand = args['0'].args.join(' '); 396 | if (!allowCommand(executedCommand)) { 397 | violations++; 398 | let violationEvent = {}; 399 | violationEvent["csp-report"] = prepareReport(arg0, 'command', stack); 400 | if (typeof reportUri === 'string') { 401 | if (violationLimitPerMin - violations >= 0) { 402 | timeoutSet(sendReport, 10, violationEvent); 403 | } 404 | } 405 | cbFunction(violationEvent, violations, violationLimitPerMinReached); 406 | } 407 | } 408 | 409 | let resourceAccessPolicyCheck = function (eventType, args, stack) { 410 | //Check if attackMonitoring is enabled. 411 | if (attackMonitoring) { 412 | switch (eventType) { 413 | case 'socket': 414 | checkSocketConnection(args, stack); 415 | break; 416 | case 'command': 417 | checkCommandExecution(args, stack); 418 | break; 419 | } 420 | } else { 421 | //todo: provide detailed INFO logs. 422 | //console.log('Attack Monitoring not enabled. '); 423 | //console.log('To enable : nodeSecurityShield.enableAttackMonitoring(resourceAccessPolicy ,callbackFunction)'); 424 | } 425 | } 426 | 427 | 428 | module.exports = { 429 | enableAttackMonitoring: enableAttackMonitoring, 430 | resourceAccessPolicyCheck: resourceAccessPolicyCheck 431 | } 432 | -------------------------------------------------------------------------------- /lib/hook.js: -------------------------------------------------------------------------------- 1 | let resourceAccessPolicyCheck = require('./attackMonitoring').resourceAccessPolicyCheck; 2 | 3 | let isHooked = false; 4 | 5 | let initHooks = function(){ 6 | if(!isHooked){ 7 | isHooked = true; 8 | hookSocket(); 9 | hookCmd(); 10 | } 11 | } 12 | 13 | let hookSocket = function(){ 14 | let methodName = "connect"; 15 | let obj = require('net').Socket.prototype; 16 | let original = obj[methodName]; 17 | obj[methodName] = function () { 18 | if (arguments.length > 0) { 19 | let stack = new Error().stack; 20 | resourceAccessPolicyCheck('socket', arguments, stack); 21 | } 22 | return original.apply(this, arguments); 23 | }; 24 | } 25 | 26 | let hookCmd = function(){ 27 | //async 28 | let methodName = "spawn"; 29 | let obj = require('child_process').ChildProcess.prototype; 30 | let original = obj[methodName]; 31 | obj[methodName] = function () { 32 | if (arguments.length > 0) { 33 | let stack = new Error().stack; 34 | resourceAccessPolicyCheck('command', arguments, stack); 35 | } 36 | return original.apply(this, arguments); 37 | }; 38 | //sync 39 | let obj1 = process.binding('spawn_sync'); 40 | let original1 = obj1[methodName]; 41 | obj1[methodName] = function () { 42 | if (arguments.length > 0) { 43 | let stack = new Error().stack; 44 | resourceAccessPolicyCheck('command', arguments, stack); 45 | } 46 | return original1.apply(this, arguments); 47 | }; 48 | 49 | } 50 | 51 | 52 | 53 | module.exports = { 54 | initHooks : initHooks 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodesecurityshield", 3 | "version": "1.1.2", 4 | "description": "A Developer and Security Engineer friendly module for Securing NodeJS Applications.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\"" 8 | }, 9 | "keywords": [ 10 | "nodejs security", 11 | "attack monitoring", 12 | "attack blocking", 13 | "scanner", 14 | "vulnerability detection", 15 | "domdog.io", 16 | "node security", 17 | "resource access policy" 18 | ], 19 | "repository": "https://github.com/DomdogSec/NodeSecurityShield", 20 | "author": { 21 | "name": "Domdog Security", 22 | "email": "sukesh@domdog.io", 23 | "url": "https://domdog.io" 24 | }, 25 | "license": "Apache License 2.0" 26 | } 27 | --------------------------------------------------------------------------------