├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build ├── build-package.ts └── helpers.ts ├── eslint.config.js ├── index.ts ├── middlewares ├── content-security-policy │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── cross-origin-embedder-policy │ └── index.ts ├── cross-origin-opener-policy │ └── index.ts ├── cross-origin-resource-policy │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── origin-agent-cluster │ └── index.ts ├── referrer-policy │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── strict-transport-security │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-content-type-options │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-dns-prefetch-control │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-download-options │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-frame-options │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-permitted-cross-domain-policies │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── x-powered-by │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json └── x-xss-protection │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package-overrides.json ├── package-lock.json ├── package.json ├── test ├── content-security-policy.test.ts ├── cross-origin-embedder-policy.test.ts ├── cross-origin-opener-policy.test.ts ├── cross-origin-resource-policy.test.ts ├── helpers.ts ├── index.test.ts ├── origin-agent-cluster.test.ts ├── project-setups.test.ts ├── project-setups │ ├── .gitignore │ ├── javascript-commonjs-default-member │ │ ├── check.js │ │ └── package.json │ ├── javascript-commonjs │ │ ├── check.js │ │ └── package.json │ ├── javascript-esm │ │ ├── check.js │ │ └── package.json │ ├── typescript-commonjs-nodenext-moduleResolution │ │ ├── check.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── typescript-commonjs │ │ ├── check.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── typescript-esnext │ │ ├── check.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── typescript-nodenext-commonjs │ │ ├── check.ts │ │ ├── package.json │ │ └── tsconfig.json │ └── typescript-nodenext-esm │ │ ├── check.ts │ │ ├── package.json │ │ └── tsconfig.json ├── referrer-policy.test.ts ├── source-files.test.ts ├── strict-transport-security.test.ts ├── x-content-type-options.test.ts ├── x-dns-prefetch-control.test.ts ├── x-download-options.test.ts ├── x-frame-options.test.ts ├── x-permitted-cross-domain-policies.test.ts ├── x-powered-by.test.ts └── x-xss-protection.test.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x, 22.x, 24.x] 15 | 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | check-latest: true 24 | - run: npm ci 25 | - run: npm test 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ 3 | .eslintcache 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others' private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [evanhahn.com/contact](https://evanhahn.com/contact). All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 74 | 75 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Helmet 2 | 3 | Helmet welcomes contributors! This guide should help you submit issues and pull requests. 4 | 5 | ## Got a question, problem, or feature request? 6 | 7 | The documentation and [Stack Overflow](https://stackoverflow.com/questions/tagged/helmet.js) are good places to start. 8 | 9 | Feel free to [add an issue](https://github.com/helmetjs/helmet/issues) or [contact the maintainer](https://evanhahn.com/contact) if those don't help! 10 | 11 | ## Want to submit a change? 12 | 13 | If you're not sure whether your change will be welcomed, [add an issue](https://github.com/helmetjs/helmet/issues) to ask. 14 | 15 | Once you're ready to make your change, make a pull request. If you're having trouble making a pull request (it's tricky!), check out [GitHub's guide](https://help.github.com/articles/using-pull-requests/) or add an issue. We'll make it work! 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012-2025 Evan Hahn, Adam Baldwin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helmet 2 | 3 | Help secure Express apps by setting HTTP response headers. 4 | 5 | ```javascript 6 | import helmet from "helmet"; 7 | 8 | const app = express(); 9 | 10 | app.use(helmet()); 11 | ``` 12 | 13 | Helmet sets the following headers by default: 14 | 15 | - [`Content-Security-Policy`](#content-security-policy): A powerful allow-list of what can happen on your page which mitigates many attacks 16 | - [`Cross-Origin-Opener-Policy`](#cross-origin-opener-policy): Helps process-isolate your page 17 | - [`Cross-Origin-Resource-Policy`](#cross-origin-resource-policy): Blocks others from loading your resources cross-origin 18 | - [`Origin-Agent-Cluster`](#origin-agent-cluster): Changes process isolation to be origin-based 19 | - [`Referrer-Policy`](#referrer-policy): Controls the [`Referer`][Referer] header 20 | - [`Strict-Transport-Security`](#strict-transport-security): Tells browsers to prefer HTTPS 21 | - [`X-Content-Type-Options`](#x-content-type-options): Avoids [MIME sniffing] 22 | - [`X-DNS-Prefetch-Control`](#x-dns-prefetch-control): Controls DNS prefetching 23 | - [`X-Download-Options`](#x-download-options): Forces downloads to be saved (Internet Explorer only) 24 | - [`X-Frame-Options`](#x-frame-options): Legacy header that mitigates [clickjacking] attacks 25 | - [`X-Permitted-Cross-Domain-Policies`](#x-permitted-cross-domain-policies): Controls cross-domain behavior for Adobe products, like Acrobat 26 | - [`X-Powered-By`](#x-powered-by): Info about the web server. Removed because it could be used in simple attacks 27 | - [`X-XSS-Protection`](#x-xss-protection): Legacy header that tries to mitigate [XSS attacks][XSS], but makes things worse, so Helmet disables it 28 | 29 | Each header can be configured. For example, here's how you configure the `Content-Security-Policy` header: 30 | 31 | ```js 32 | // Configure the Content-Security-Policy header. 33 | app.use( 34 | helmet({ 35 | contentSecurityPolicy: { 36 | directives: { 37 | "script-src": ["'self'", "example.com"], 38 | }, 39 | }, 40 | }), 41 | ); 42 | ``` 43 | 44 | Headers can also be disabled. For example, here's how you disable the `Content-Security-Policy` and `X-Download-Options` headers: 45 | 46 | ```js 47 | // Disable the Content-Security-Policy and X-Download-Options headers 48 | app.use( 49 | helmet({ 50 | contentSecurityPolicy: false, 51 | xDownloadOptions: false, 52 | }), 53 | ); 54 | ``` 55 | 56 | ## Reference 57 | 58 |
59 | Content-Security-Policy 60 | 61 | Default: 62 | 63 | ```http 64 | Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests 65 | ``` 66 | 67 | The `Content-Security-Policy` header mitigates a large number of attacks, such as [cross-site scripting][XSS]. See [MDN's introductory article on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). 68 | 69 | This header is powerful but likely requires some configuration for your specific app. 70 | 71 | To configure this header, pass an object with a nested `directives` object. Each key is a directive name in camel case (such as `defaultSrc`) or kebab case (such as `default-src`). Each value is an array (or other iterable) of strings or functions for that directive. If a function appears in the array, it will be called with the request and response objects. 72 | 73 | ```javascript 74 | // Sets all of the defaults, but overrides `script-src` 75 | // and disables the default `style-src`. 76 | app.use( 77 | helmet({ 78 | contentSecurityPolicy: { 79 | directives: { 80 | "script-src": ["'self'", "example.com"], 81 | "style-src": null, 82 | }, 83 | }, 84 | }), 85 | ); 86 | ``` 87 | 88 | ```js 89 | // Sets the `script-src` directive to 90 | // "'self' 'nonce-e33cc...'" 91 | // (or similar) 92 | app.use((req, res, next) => { 93 | res.locals.cspNonce = crypto.randomBytes(32).toString("hex"); 94 | next(); 95 | }); 96 | app.use( 97 | helmet({ 98 | contentSecurityPolicy: { 99 | directives: { 100 | scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`], 101 | }, 102 | }, 103 | }), 104 | ); 105 | ``` 106 | 107 | These directives are merged into a default policy, which you can disable by setting `useDefaults` to `false`. 108 | 109 | ```javascript 110 | // Sets "Content-Security-Policy: default-src 'self'; 111 | // script-src 'self' example.com;object-src 'none'; 112 | // upgrade-insecure-requests" 113 | app.use( 114 | helmet({ 115 | contentSecurityPolicy: { 116 | useDefaults: false, 117 | directives: { 118 | defaultSrc: ["'self'"], 119 | scriptSrc: ["'self'", "example.com"], 120 | objectSrc: ["'none'"], 121 | upgradeInsecureRequests: [], 122 | }, 123 | }, 124 | }), 125 | ); 126 | ``` 127 | 128 | You can get the default directives object with `helmet.contentSecurityPolicy.getDefaultDirectives()`. Here is the default policy (formatted for readability): 129 | 130 | ``` 131 | default-src 'self'; 132 | base-uri 'self'; 133 | font-src 'self' https: data:; 134 | form-action 'self'; 135 | frame-ancestors 'self'; 136 | img-src 'self' data:; 137 | object-src 'none'; 138 | script-src 'self'; 139 | script-src-attr 'none'; 140 | style-src 'self' https: 'unsafe-inline'; 141 | upgrade-insecure-requests 142 | ``` 143 | 144 | The `default-src` directive can be explicitly disabled by setting its value to `helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc`, but this is not recommended. 145 | 146 | You can set the [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) instead: 147 | 148 | ```javascript 149 | // Sets the Content-Security-Policy-Report-Only header 150 | app.use( 151 | helmet({ 152 | contentSecurityPolicy: { 153 | directives: { 154 | /* ... */ 155 | }, 156 | reportOnly: true, 157 | }, 158 | }), 159 | ); 160 | ``` 161 | 162 | Helmet performs very little validation on your CSP. You should rely on CSP checkers like [CSP Evaluator](https://csp-evaluator.withgoogle.com/) instead. 163 | 164 | To disable the `Content-Security-Policy` header: 165 | 166 | ```js 167 | app.use( 168 | helmet({ 169 | contentSecurityPolicy: false, 170 | }), 171 | ); 172 | ``` 173 | 174 | You can use this as standalone middleware with `app.use(helmet.contentSecurityPolicy())`. 175 | 176 |
177 | 178 |
179 | Cross-Origin-Embedder-Policy 180 | 181 | This header is not set by default. 182 | 183 | The `Cross-Origin-Embedder-Policy` header helps control what resources can be loaded cross-origin. See [MDN's article on this header](https://developer.cdn.mozilla.net/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) for more. 184 | 185 | ```js 186 | // Helmet does not set Cross-Origin-Embedder-Policy 187 | // by default. 188 | app.use(helmet()); 189 | 190 | // Sets "Cross-Origin-Embedder-Policy: require-corp" 191 | app.use(helmet({ crossOriginEmbedderPolicy: true })); 192 | 193 | // Sets "Cross-Origin-Embedder-Policy: credentialless" 194 | app.use(helmet({ crossOriginEmbedderPolicy: { policy: "credentialless" } })); 195 | ``` 196 | 197 | You can use this as standalone middleware with `app.use(helmet.crossOriginEmbedderPolicy())`. 198 | 199 |
200 | 201 |
202 | Cross-Origin-Opener-Policy 203 | 204 | Default: 205 | 206 | ```http 207 | Cross-Origin-Opener-Policy: same-origin 208 | ``` 209 | 210 | The `Cross-Origin-Opener-Policy` header helps process-isolate your page. For more, see [MDN's article on this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy). 211 | 212 | ```js 213 | // Sets "Cross-Origin-Opener-Policy: same-origin" 214 | app.use(helmet()); 215 | 216 | // Sets "Cross-Origin-Opener-Policy: same-origin-allow-popups" 217 | app.use( 218 | helmet({ 219 | crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" }, 220 | }), 221 | ); 222 | ``` 223 | 224 | To disable the `Cross-Origin-Opener-Policy` header: 225 | 226 | ```js 227 | app.use( 228 | helmet({ 229 | crossOriginOpenerPolicy: false, 230 | }), 231 | ); 232 | ``` 233 | 234 | You can use this as standalone middleware with `app.use(helmet.crossOriginOpenerPolicy())`. 235 | 236 |
237 | 238 |
239 | Cross-Origin-Resource-Policy 240 | 241 | Default: 242 | 243 | ```http 244 | Cross-Origin-Resource-Policy: same-origin 245 | ``` 246 | 247 | The `Cross-Origin-Resource-Policy` header blocks others from loading your resources cross-origin in some cases. For more, see ["Consider deploying Cross-Origin Resource Policy"](https://resourcepolicy.fyi/) and [MDN's article on this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy). 248 | 249 | ```js 250 | // Sets "Cross-Origin-Resource-Policy: same-origin" 251 | app.use(helmet()); 252 | 253 | // Sets "Cross-Origin-Resource-Policy: same-site" 254 | app.use(helmet({ crossOriginResourcePolicy: { policy: "same-site" } })); 255 | ``` 256 | 257 | To disable the `Cross-Origin-Resource-Policy` header: 258 | 259 | ```js 260 | app.use( 261 | helmet({ 262 | crossOriginResourcePolicy: false, 263 | }), 264 | ); 265 | ``` 266 | 267 | You can use this as standalone middleware with `app.use(helmet.crossOriginResourcePolicy())`. 268 | 269 |
270 | 271 |
272 | Origin-Agent-Cluster 273 | 274 | Default: 275 | 276 | ```http 277 | Origin-Agent-Cluster: ?1 278 | ``` 279 | 280 | The `Origin-Agent-Cluster` header provides a mechanism to allow web applications to isolate their origins from other processes. Read more about it [in the spec](https://whatpr.org/html/6214/origin.html#origin-keyed-agent-clusters). 281 | 282 | This header takes no options and is set by default. 283 | 284 | ```js 285 | // Sets "Origin-Agent-Cluster: ?1" 286 | app.use(helmet()); 287 | ``` 288 | 289 | To disable the `Origin-Agent-Cluster` header: 290 | 291 | ```js 292 | app.use( 293 | helmet({ 294 | originAgentCluster: false, 295 | }), 296 | ); 297 | ``` 298 | 299 | You can use this as standalone middleware with `app.use(helmet.originAgentCluster())`. 300 | 301 |
302 | 303 |
304 | Referrer-Policy 305 | 306 | Default: 307 | 308 | ```http 309 | Referrer-Policy: no-referrer 310 | ``` 311 | 312 | The `Referrer-Policy` header which controls what information is set in [the `Referer` request header][Referer]. See ["Referer header: privacy and security concerns"](https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns) and [the header's documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) on MDN for more. 313 | 314 | ```js 315 | // Sets "Referrer-Policy: no-referrer" 316 | app.use(helmet()); 317 | ``` 318 | 319 | `policy` is a string or array of strings representing the policy. If passed as an array, it will be joined with commas, which is useful when setting [a fallback policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#Specifying_a_fallback_policy). It defaults to `no-referrer`. 320 | 321 | ```js 322 | // Sets "Referrer-Policy: no-referrer" 323 | app.use( 324 | helmet({ 325 | referrerPolicy: { 326 | policy: "no-referrer", 327 | }, 328 | }), 329 | ); 330 | 331 | // Sets "Referrer-Policy: origin,unsafe-url" 332 | app.use( 333 | helmet({ 334 | referrerPolicy: { 335 | policy: ["origin", "unsafe-url"], 336 | }, 337 | }), 338 | ); 339 | ``` 340 | 341 | To disable the `Referrer-Policy` header: 342 | 343 | ```js 344 | app.use( 345 | helmet({ 346 | referrerPolicy: false, 347 | }), 348 | ); 349 | ``` 350 | 351 | You can use this as standalone middleware with `app.use(helmet.referrerPolicy())`. 352 | 353 |
354 | 355 |
356 | Strict-Transport-Security 357 | 358 | Default: 359 | 360 | ```http 361 | Strict-Transport-Security: max-age=31536000; includeSubDomains 362 | ``` 363 | 364 | The `Strict-Transport-Security` header tells browsers to prefer HTTPS instead of insecure HTTP. See [the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) for more. 365 | 366 | ```js 367 | // Sets "Strict-Transport-Security: max-age=31536000; includeSubDomains" 368 | app.use(helmet()); 369 | ``` 370 | 371 | `maxAge` is the number of seconds browsers should remember to prefer HTTPS. If passed a non-integer, the value is rounded down. It defaults to 365 days. 372 | 373 | `includeSubDomains` is a boolean which dictates whether to include the `includeSubDomains` directive, which makes this policy extend to subdomains. It defaults to `true`. 374 | 375 | `preload` is a boolean. If true, it adds the `preload` directive, expressing intent to add your HSTS policy to browsers. See [the "Preloading Strict Transport Security" section on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security) for more. It defaults to `false`. 376 | 377 | ```js 378 | // Sets "Strict-Transport-Security: max-age=123456; includeSubDomains" 379 | app.use( 380 | helmet({ 381 | strictTransportSecurity: { 382 | maxAge: 123456, 383 | }, 384 | }), 385 | ); 386 | 387 | // Sets "Strict-Transport-Security: max-age=123456" 388 | app.use( 389 | helmet({ 390 | strictTransportSecurity: { 391 | maxAge: 123456, 392 | includeSubDomains: false, 393 | }, 394 | }), 395 | ); 396 | 397 | // Sets "Strict-Transport-Security: max-age=123456; includeSubDomains; preload" 398 | app.use( 399 | helmet({ 400 | strictTransportSecurity: { 401 | maxAge: 63072000, 402 | preload: true, 403 | }, 404 | }), 405 | ); 406 | ``` 407 | 408 | To disable the `Strict-Transport-Security` header: 409 | 410 | ```js 411 | app.use( 412 | helmet({ 413 | strictTransportSecurity: false, 414 | }), 415 | ); 416 | ``` 417 | 418 | You may wish to disable this header for local development, as it can make your browser force redirects from `http://localhost` to `https://localhost`, which may not be desirable if you develop multiple apps using `localhost`. See [this issue](https://github.com/helmetjs/helmet/issues/451) for more discussion. 419 | 420 | You can use this as standalone middleware with `app.use(helmet.strictTransportSecurity())`. 421 | 422 |
423 | 424 |
425 | X-Content-Type-Options 426 | 427 | Default: 428 | 429 | ```http 430 | X-Content-Type-Options: nosniff 431 | ``` 432 | 433 | The `X-Content-Type-Options` mitigates [MIME type sniffing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing) which can cause security issues. See [documentation for this header on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) for more. 434 | 435 | This header takes no options and is set by default. 436 | 437 | ```js 438 | // Sets "X-Content-Type-Options: nosniff" 439 | app.use(helmet()); 440 | ``` 441 | 442 | To disable the `X-Content-Type-Options` header: 443 | 444 | ```js 445 | app.use( 446 | helmet({ 447 | xContentTypeOptions: false, 448 | }), 449 | ); 450 | ``` 451 | 452 | You can use this as standalone middleware with `app.use(helmet.xContentTypeOptions())`. 453 | 454 |
455 | 456 |
457 | X-DNS-Prefetch-Control 458 | 459 | Default: 460 | 461 | ```http 462 | X-DNS-Prefetch-Control: off 463 | ``` 464 | 465 | The `X-DNS-Prefetch-Control` header helps control DNS prefetching, which can improve user privacy at the expense of performance. See [documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) for more. 466 | 467 | ```js 468 | // Sets "X-DNS-Prefetch-Control: off" 469 | app.use(helmet()); 470 | ``` 471 | 472 | `allow` is a boolean dictating whether to enable DNS prefetching. It defaults to `false`. 473 | 474 | Examples: 475 | 476 | ```js 477 | // Sets "X-DNS-Prefetch-Control: off" 478 | app.use( 479 | helmet({ 480 | xDnsPrefetchControl: { allow: false }, 481 | }), 482 | ); 483 | 484 | // Sets "X-DNS-Prefetch-Control: on" 485 | app.use( 486 | helmet({ 487 | xDnsPrefetchControl: { allow: true }, 488 | }), 489 | ); 490 | ``` 491 | 492 | To disable the `X-DNS-Prefetch-Control` header and use the browser's default value: 493 | 494 | ```js 495 | app.use( 496 | helmet({ 497 | xDnsPrefetchControl: false, 498 | }), 499 | ); 500 | ``` 501 | 502 | You can use this as standalone middleware with `app.use(helmet.xDnsPrefetchControl())`. 503 | 504 |
505 | 506 |
507 | X-Download-Options 508 | 509 | Default: 510 | 511 | ```http 512 | X-Download-Options: noopen 513 | ``` 514 | 515 | The `X-Download-Options` header is specific to Internet Explorer 8. It forces potentially-unsafe downloads to be saved, mitigating execution of HTML in your site's context. For more, see [this old post on MSDN](https://docs.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection). 516 | 517 | This header takes no options and is set by default. 518 | 519 | ```js 520 | // Sets "X-Download-Options: noopen" 521 | app.use(helmet()); 522 | ``` 523 | 524 | To disable the `X-Download-Options` header: 525 | 526 | ```js 527 | app.use( 528 | helmet({ 529 | xDownloadOptions: false, 530 | }), 531 | ); 532 | ``` 533 | 534 | You can use this as standalone middleware with `app.use(helmet.xDownloadOptions())`. 535 | 536 |
537 | 538 |
539 | X-Frame-Options 540 | 541 | Default: 542 | 543 | ```http 544 | X-Frame-Options: SAMEORIGIN 545 | ``` 546 | 547 | The legacy `X-Frame-Options` header to help you mitigate [clickjacking attacks](https://en.wikipedia.org/wiki/Clickjacking). This header is superseded by [the `frame-ancestors` Content Security Policy directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) but is still useful on old browsers or if no CSP is used. For more, see [the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). 548 | 549 | ```js 550 | // Sets "X-Frame-Options: SAMEORIGIN" 551 | app.use(helmet()); 552 | ``` 553 | 554 | `action` is a string that specifies which directive to use—either `DENY` or `SAMEORIGIN`. (A legacy directive, `ALLOW-FROM`, is not supported by Helmet. [Read more here.](https://github.com/helmetjs/helmet/wiki/How-to-use-X%E2%80%93Frame%E2%80%93Options's-%60ALLOW%E2%80%93FROM%60-directive)) It defaults to `SAMEORIGIN`. 555 | 556 | Examples: 557 | 558 | ```js 559 | // Sets "X-Frame-Options: DENY" 560 | app.use( 561 | helmet({ 562 | xFrameOptions: { action: "deny" }, 563 | }), 564 | ); 565 | 566 | // Sets "X-Frame-Options: SAMEORIGIN" 567 | app.use( 568 | helmet({ 569 | xFrameOptions: { action: "sameorigin" }, 570 | }), 571 | ); 572 | ``` 573 | 574 | To disable the `X-Frame-Options` header: 575 | 576 | ```js 577 | app.use( 578 | helmet({ 579 | xFrameOptions: false, 580 | }), 581 | ); 582 | ``` 583 | 584 | You can use this as standalone middleware with `app.use(helmet.xFrameOptions())`. 585 | 586 |
587 | 588 |
589 | X-Permitted-Cross-Domain-Policies 590 | 591 | Default: 592 | 593 | ```http 594 | X-Permitted-Cross-Domain-Policies: none 595 | ``` 596 | 597 | The `X-Permitted-Cross-Domain-Policies` header tells some clients (mostly Adobe products) your domain's policy for loading cross-domain content. See [the description on OWASP](https://owasp.org/www-project-secure-headers/) for more. 598 | 599 | ```js 600 | // Sets "X-Permitted-Cross-Domain-Policies: none" 601 | app.use(helmet()); 602 | ``` 603 | 604 | `permittedPolicies` is a string that must be `"none"`, `"master-only"`, `"by-content-type"`, or `"all"`. It defaults to `"none"`. 605 | 606 | Examples: 607 | 608 | ```js 609 | // Sets "X-Permitted-Cross-Domain-Policies: none" 610 | app.use( 611 | helmet({ 612 | xPermittedCrossDomainPolicies: { 613 | permittedPolicies: "none", 614 | }, 615 | }), 616 | ); 617 | 618 | // Sets "X-Permitted-Cross-Domain-Policies: by-content-type" 619 | app.use( 620 | helmet({ 621 | xPermittedCrossDomainPolicies: { 622 | permittedPolicies: "by-content-type", 623 | }, 624 | }), 625 | ); 626 | ``` 627 | 628 | To disable the `X-Permitted-Cross-Domain-Policies` header: 629 | 630 | ```js 631 | app.use( 632 | helmet({ 633 | xPermittedCrossDomainPolicies: false, 634 | }), 635 | ); 636 | ``` 637 | 638 | You can use this as standalone middleware with `app.use(helmet.xPermittedCrossDomainPolicies())`. 639 | 640 |
641 | 642 |
643 | X-Powered-By 644 | 645 | Default: the `X-Powered-By` header, if present, is removed. 646 | 647 | Helmet removes the `X-Powered-By` header, which is set by default in Express and some other frameworks. Removing the header offers very limited security benefits (see [this discussion](https://github.com/expressjs/express/pull/2813#issuecomment-159270428)) and is mostly removed to save bandwidth, but may thwart simplistic attackers. 648 | 649 | Note: [Express has a built-in way to disable the `X-Powered-By` header](https://stackoverflow.com/a/12484642/804100), which you may wish to use instead. 650 | 651 | The removal of this header takes no options. The header is removed by default. 652 | 653 | To disable this behavior: 654 | 655 | ```js 656 | // Not required, but recommended for Express users: 657 | app.disable("x-powered-by"); 658 | 659 | // Ask Helmet to ignore the X-Powered-By header. 660 | app.use( 661 | helmet({ 662 | xPoweredBy: false, 663 | }), 664 | ); 665 | ``` 666 | 667 | You can use this as standalone middleware with `app.use(helmet.xPoweredBy())`. 668 | 669 |
670 | 671 |
672 | X-XSS-Protection 673 | 674 | Default: 675 | 676 | ```http 677 | X-XSS-Protection: 0 678 | ``` 679 | 680 | Helmet disables browsers' buggy cross-site scripting filter by setting the legacy `X-XSS-Protection` header to `0`. See [discussion about disabling the header here](https://github.com/helmetjs/helmet/issues/230) and [documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection). 681 | 682 | This header takes no options and is set by default. 683 | 684 | To disable the `X-XSS-Protection` header: 685 | 686 | ```js 687 | // This is not recommended. 688 | app.use( 689 | helmet({ 690 | xXssProtection: false, 691 | }), 692 | ); 693 | ``` 694 | 695 | You can use this as standalone middleware with `app.use(helmet.xXssProtection())`. 696 | 697 |
698 | 699 | [Referer]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer 700 | [MIME sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing 701 | [Clickjacking]: https://en.wikipedia.org/wiki/Clickjacking 702 | [XSS]: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting 703 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security issue reporting & disclosure process 2 | 3 | If you feel you have found a security issue or concern with Helmet, please reach out to the maintainers. 4 | 5 | Contact Evan Hahn at or Adam Baldwin at . Evan Hahn [can also be reached in other ways](https://evanhahn.com/contact). 6 | 7 | We will try to communicate in a timely manner and address your concerns. 8 | -------------------------------------------------------------------------------- /build/build-package.ts: -------------------------------------------------------------------------------- 1 | import rollupTypescript from "@rollup/plugin-typescript"; 2 | import zopfli from "node-zopfli"; 3 | import * as fsOriginal from "node:fs"; 4 | import * as os from "node:os"; 5 | import * as path from "node:path"; 6 | import { pipeline } from "node:stream"; 7 | import { fileURLToPath } from "node:url"; 8 | import { promisify } from "node:util"; 9 | import * as zlib from "node:zlib"; 10 | import prettier from "prettier"; 11 | import { type RollupBuild, rollup } from "rollup"; 12 | import rollupDts from "rollup-plugin-dts"; 13 | import { npm } from "./helpers.js"; 14 | 15 | const fs = fsOriginal.promises; 16 | const pipe = promisify(pipeline); 17 | 18 | // This shaves a few bytes off the built files while still keeping them readable. 19 | // When testing on 4f550aab7ccf00a6dfe686d57195268b3ef06b1a, it reduces the tarball size by about 100 bytes. 20 | // This should help installation performance slightly. 21 | const PRETTIER_PREPACK_CRUSH_OPTIONS: prettier.Options = { 22 | printWidth: 2000, 23 | trailingComma: "none", 24 | useTabs: true, 25 | arrowParens: "avoid", 26 | bracketSpacing: false, 27 | semi: false, 28 | }; 29 | 30 | const rootDir = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); 31 | const testFiles = path.join(rootDir, "test", "**"); 32 | 33 | /** 34 | * Build a Helmet package into a tarball ready to be published with `npm publish`. 35 | */ 36 | export async function buildAndPack( 37 | middlewareToBuild?: string, 38 | ): Promise { 39 | let entry: string; 40 | let esm: boolean; 41 | let packageOverrides: Record; 42 | let filesToCopy: readonly string[]; 43 | 44 | if (middlewareToBuild) { 45 | const middlewareDir = path.join(rootDir, "middlewares", middlewareToBuild); 46 | entry = path.join(middlewareDir, "index.ts"); 47 | esm = false; 48 | packageOverrides = await readJsonObject( 49 | path.join(middlewareDir, "package-overrides.json"), 50 | ); 51 | filesToCopy = [ 52 | path.join(rootDir, "LICENSE"), 53 | path.join(middlewareDir, "README.md"), 54 | path.join(middlewareDir, "CHANGELOG.md"), 55 | ]; 56 | } else { 57 | entry = path.join(rootDir, "index.ts"); 58 | esm = true; 59 | packageOverrides = {}; 60 | filesToCopy = [ 61 | path.join(rootDir, "LICENSE"), 62 | path.join(rootDir, "README.md"), 63 | path.join(rootDir, "CHANGELOG.md"), 64 | path.join(rootDir, "SECURITY.md"), 65 | ]; 66 | } 67 | 68 | const distDir = await temporaryDirectory(); 69 | 70 | await Promise.all([ 71 | buildCjs({ entry, distDir }), 72 | ...(esm ? [buildMjs({ entry, distDir })] : []), 73 | buildTypes({ esm, entry, distDir }), 74 | buildPackageJson({ esm, packageOverrides, distDir }), 75 | copyStaticFiles({ filesToCopy, distDir }), 76 | ]); 77 | 78 | await prePackCrush(distDir); 79 | 80 | const npmPackedTarball = await pack(distDir); 81 | 82 | const crushedTarball = await postPackCrush(npmPackedTarball); 83 | 84 | return crushedTarball; 85 | } 86 | 87 | function temporaryDirectory(): Promise { 88 | return fs.mkdtemp(path.join(os.tmpdir(), "helmet")); 89 | } 90 | 91 | async function buildCjs({ 92 | entry, 93 | distDir, 94 | }: Readonly<{ entry: string; distDir: string }>) { 95 | const outputPath = path.join(distDir, "index.cjs"); 96 | 97 | console.log(`Building ${outputPath}...`); 98 | 99 | const bundle = await rollupForJs({ entry, distDir }); 100 | 101 | await bundle.write({ 102 | file: outputPath, 103 | exports: "named", 104 | format: "cjs", 105 | generatedCode: "es2015", 106 | outro: [ 107 | "module.exports = exports.default;", 108 | // Some bundlers import with CommonJS but then pull `default` off. 109 | "module.exports.default = module.exports;", 110 | ].join("\n"), 111 | }); 112 | 113 | await bundle.close(); 114 | 115 | console.log(`Built ${outputPath}.`); 116 | } 117 | 118 | async function buildMjs({ 119 | entry, 120 | distDir, 121 | }: Readonly<{ entry: string; distDir: string }>) { 122 | const outputPath = path.join(distDir, "index.mjs"); 123 | 124 | console.log(`Building ${outputPath}...`); 125 | 126 | const bundle = await rollupForJs({ entry, distDir }); 127 | 128 | await bundle.write({ 129 | file: outputPath, 130 | format: "esm", 131 | generatedCode: "es2015", 132 | }); 133 | 134 | await bundle.close(); 135 | 136 | console.log(`Built ${outputPath}.`); 137 | } 138 | 139 | function rollupForJs({ 140 | entry, 141 | distDir, 142 | }: Readonly<{ entry: string; distDir: string }>): Promise { 143 | return rollup({ 144 | input: entry, 145 | plugins: [ 146 | rollupTypescript({ 147 | outDir: distDir, 148 | exclude: [testFiles], 149 | }), 150 | ], 151 | }); 152 | } 153 | 154 | async function buildTypes({ 155 | esm, 156 | entry, 157 | distDir, 158 | }: Readonly<{ esm: boolean; entry: string; distDir: string }>) { 159 | console.log("Building types..."); 160 | 161 | const bundle = await rollup({ 162 | input: entry, 163 | external: ["node:http"], 164 | plugins: [rollupDts()], 165 | }); 166 | 167 | await Promise.all([ 168 | (async () => { 169 | const cjsPath = path.join(distDir, "index.d.cts"); 170 | await bundle.write({ 171 | file: cjsPath, 172 | format: "commonjs", 173 | }); 174 | console.log(`Built ${cjsPath}.`); 175 | })(), 176 | (async () => { 177 | if (!esm) { 178 | return; 179 | } 180 | const esmPath = path.join(distDir, "index.d.mts"); 181 | await bundle.write({ 182 | file: esmPath, 183 | format: "esm", 184 | }); 185 | console.log(`Built ${esmPath}.`); 186 | })(), 187 | ]); 188 | 189 | await bundle.close(); 190 | } 191 | 192 | async function buildPackageJson({ 193 | esm, 194 | packageOverrides, 195 | distDir, 196 | }: Readonly<{ 197 | esm: boolean; 198 | packageOverrides: Readonly>; 199 | distDir: string; 200 | }>) { 201 | const outputPath = path.join(distDir, "package.json"); 202 | 203 | console.log(`Building ${outputPath}...`); 204 | 205 | const devPackageJson = await readJsonObject( 206 | path.join(rootDir, "package.json"), 207 | ); 208 | 209 | const packageJson = { 210 | name: "helmet", 211 | description: "help secure Express/Connect apps with various HTTP headers", 212 | version: devPackageJson.version, 213 | author: "Adam Baldwin (https://evilpacket.net)", 214 | contributors: ["Evan Hahn (https://evanhahn.com)"], 215 | homepage: "https://helmetjs.github.io/", 216 | bugs: { 217 | url: "https://github.com/helmetjs/helmet/issues", 218 | email: "me@evanhahn.com", 219 | }, 220 | repository: { 221 | type: "git", 222 | url: "git://github.com/helmetjs/helmet.git", 223 | }, 224 | license: "MIT", 225 | keywords: [ 226 | "express", 227 | "security", 228 | "headers", 229 | "backend", 230 | "content-security-policy", 231 | "cross-origin-embedder-policy", 232 | "cross-origin-opener-policy", 233 | "cross-origin-resource-policy", 234 | "origin-agent-cluster", 235 | "referrer-policy", 236 | "strict-transport-security", 237 | "x-content-type-options", 238 | "x-dns-prefetch-control", 239 | "x-download-options", 240 | "x-frame-options", 241 | "x-permitted-cross-domain-policies", 242 | "x-powered-by", 243 | "x-xss-protection", 244 | ], 245 | engines: devPackageJson.engines, 246 | exports: { 247 | ...(esm ? { import: "./index.mjs" } : {}), 248 | require: "./index.cjs", 249 | }, 250 | // All supported versions of Node handle `exports`, but some build tools 251 | // still use `main`, so we keep it around. 252 | main: "./index.cjs", 253 | 254 | // Support old TypeScript versions. 255 | types: "./index.d.cts", 256 | 257 | ...packageOverrides, 258 | }; 259 | 260 | await fs.writeFile(outputPath, JSON.stringify(packageJson)); 261 | 262 | console.log(`Built ${outputPath}.`); 263 | } 264 | 265 | async function readJsonObject( 266 | path: fsOriginal.PathLike, 267 | ): Promise> { 268 | const result: unknown = JSON.parse(await fs.readFile(path, "utf8")); 269 | if (typeof result !== "object" || result === null) { 270 | throw new Error("Got a non-object from JSON.parse()"); 271 | } 272 | return result as Record; 273 | } 274 | 275 | async function copyStaticFiles({ 276 | filesToCopy, 277 | distDir, 278 | }: Readonly<{ filesToCopy: readonly string[]; distDir: string }>) { 279 | await Promise.all( 280 | filesToCopy.map(async (source) => { 281 | const basename = path.basename(source); 282 | const dest = path.join(distDir, basename); 283 | console.log(`Copying ${source} to ${dest}...`); 284 | await fs.copyFile(source, dest); 285 | console.log(`Copied ${source} to ${dest}.`); 286 | }), 287 | ); 288 | } 289 | 290 | async function prePackCrush(distDir: string): Promise { 291 | const files = (await fs.readdir(distDir)) 292 | .map((file) => path.join(distDir, file)) 293 | .filter((file) => path.extname(file) !== ".md"); 294 | 295 | await Promise.all( 296 | files.map(async (file) => { 297 | const prettierInfo = await prettier.getFileInfo(file); 298 | if (!prettierInfo.inferredParser) { 299 | return; 300 | } 301 | 302 | console.log(`Crushing ${file}...`); 303 | 304 | const oldContents = await fs.readFile(file, { encoding: "utf8" }); 305 | 306 | const newContents = await prettier.format(oldContents, { 307 | filepath: file, 308 | ...PRETTIER_PREPACK_CRUSH_OPTIONS, 309 | }); 310 | 311 | await fs.writeFile(file, newContents, { encoding: "utf8" }); 312 | 313 | console.log(`Crushed ${file}.`); 314 | }), 315 | ); 316 | } 317 | 318 | async function pack(distDir: string): Promise { 319 | await npm(["pack"], { cwd: distDir }); 320 | 321 | const tempDirFiles = await fs.readdir(distDir); 322 | const tarballName = tempDirFiles.find( 323 | (file) => file.startsWith("helmet-") && file.endsWith(".tgz"), 324 | ); 325 | if (!tarballName) { 326 | throw new Error( 327 | "Couldn't find helmet tarball in temp directory. Build is not set up correctly", 328 | ); 329 | } 330 | 331 | return path.join(distDir, tarballName); 332 | } 333 | 334 | async function postPackCrush(originalTarGz: string): Promise { 335 | const originalSize = (await fs.stat(originalTarGz)).size; 336 | console.log(`Crushing ${originalTarGz} (size: ${originalSize})...`); 337 | 338 | const crushedTarGz = originalTarGz.replace(".tgz", ".crushed.tgz"); 339 | const readOriginal = fsOriginal.createReadStream(originalTarGz); 340 | const gunzip = zlib.createGunzip(); 341 | const gzip = zopfli.createGzip({ numiterations: 100 }); 342 | const writeCrushed = fsOriginal.createWriteStream(crushedTarGz); 343 | 344 | await pipe(readOriginal, gunzip, gzip, writeCrushed); 345 | 346 | const crushedSize = (await fs.stat(crushedTarGz)).size; 347 | const savings = originalSize - crushedSize; 348 | 349 | if (savings < 0) { 350 | console.log("Original tarball was smaller"); 351 | return originalTarGz; 352 | } else { 353 | const ratio = crushedSize / originalSize; 354 | console.log( 355 | `Crushed into ${crushedTarGz}. Size: ${crushedSize}. Savings: ${savings} bytes (result is ${Math.round( 356 | ratio * 100, 357 | )}% the size)`, 358 | ); 359 | return crushedTarGz; 360 | } 361 | } 362 | 363 | const isMain = 364 | import.meta.url.startsWith("file:") && 365 | process.argv[1] === fileURLToPath(import.meta.url); 366 | if (isMain) { 367 | if (process.argv.length > 3) { 368 | throw new Error("Too many arguments"); 369 | } 370 | 371 | const middlewareToBuild = process.argv[2]; 372 | buildAndPack(middlewareToBuild) 373 | .then((finalTarballPath) => { 374 | console.log(finalTarballPath); 375 | }) 376 | .catch((err) => { 377 | console.error(err); 378 | process.exit(1); 379 | }); 380 | } 381 | -------------------------------------------------------------------------------- /build/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from "node:child_process"; 2 | 3 | export const npm = ( 4 | args: readonly string[], 5 | { cwd }: Readonly<{ cwd: string }>, 6 | ): Promise => 7 | new Promise((resolve, reject) => { 8 | const proc = childProcess.spawn("npm", args, { 9 | cwd, 10 | stdio: ["inherit", "ignore", "inherit"], 11 | }); 12 | proc.on("close", (code) => { 13 | if (code === 0) { 14 | resolve(); 15 | } else { 16 | reject(new Error(`npm ${args.join(" ")} exited with code ${code}`)); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginJs from "@eslint/js"; 2 | import globals from "globals"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | export default [ 6 | { 7 | files: ["**/*.{js,mjs,cjs,ts}"], 8 | }, 9 | { languageOptions: { globals: globals.node } }, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.strict, 12 | { 13 | ignores: ["*.config.js", "coverage/**/**"], 14 | }, 15 | { 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: { 19 | allowDefaultProject: [ 20 | "test/project-setups/javascript-*/*.{js,mjs,cjs,ts}", 21 | ], 22 | }, 23 | tsconfigRootDir: import.meta.dirname, 24 | }, 25 | }, 26 | }, 27 | { 28 | rules: { 29 | "no-unused-vars": "off", 30 | "@typescript-eslint/consistent-type-exports": "error", 31 | "@typescript-eslint/consistent-type-imports": "error", 32 | "@typescript-eslint/no-confusing-void-expression": "error", 33 | "@typescript-eslint/no-duplicate-enum-values": "error", 34 | "@typescript-eslint/no-duplicate-type-constituents": "error", 35 | "@typescript-eslint/no-extraneous-class": "error", 36 | "@typescript-eslint/no-import-type-side-effects": "error", 37 | "@typescript-eslint/no-redundant-type-constituents": "error", 38 | "@typescript-eslint/no-require-imports": "error", 39 | "@typescript-eslint/no-unnecessary-condition": "error", 40 | "@typescript-eslint/no-unnecessary-qualifier": "error", 41 | "@typescript-eslint/no-unused-vars": "error", 42 | "@typescript-eslint/no-useless-empty-export": "error", 43 | "@typescript-eslint/prefer-readonly": "error", 44 | "@typescript-eslint/prefer-regexp-exec": "error", 45 | "@typescript-eslint/require-array-sort-compare": "error", 46 | "@typescript-eslint/switch-exhaustiveness-check": "error", 47 | }, 48 | }, 49 | { 50 | files: ["test/**/*.{js,mjs,cjs,ts}"], 51 | rules: { 52 | "@typescript-eslint/no-explicit-any": "off", 53 | "@typescript-eslint/no-unsafe-argument": "off", 54 | "@typescript-eslint/no-unsafe-assignment": "off", 55 | }, 56 | }, 57 | { 58 | files: ["test/project-setups/**/*.{js,mjs,cjs,ts}"], 59 | rules: { 60 | "@typescript-eslint/no-require-imports": "off", 61 | "@typescript-eslint/no-unnecessary-condition": "off", 62 | }, 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | import contentSecurityPolicy, { 3 | type ContentSecurityPolicyOptions, 4 | } from "./middlewares/content-security-policy/index.js"; 5 | import crossOriginEmbedderPolicy, { 6 | type CrossOriginEmbedderPolicyOptions, 7 | } from "./middlewares/cross-origin-embedder-policy/index.js"; 8 | import crossOriginOpenerPolicy, { 9 | type CrossOriginOpenerPolicyOptions, 10 | } from "./middlewares/cross-origin-opener-policy/index.js"; 11 | import crossOriginResourcePolicy, { 12 | type CrossOriginResourcePolicyOptions, 13 | } from "./middlewares/cross-origin-resource-policy/index.js"; 14 | import originAgentCluster from "./middlewares/origin-agent-cluster/index.js"; 15 | import referrerPolicy, { 16 | type ReferrerPolicyOptions, 17 | } from "./middlewares/referrer-policy/index.js"; 18 | import strictTransportSecurity, { 19 | type StrictTransportSecurityOptions, 20 | } from "./middlewares/strict-transport-security/index.js"; 21 | import xContentTypeOptions from "./middlewares/x-content-type-options/index.js"; 22 | import xDnsPrefetchControl, { 23 | type XDnsPrefetchControlOptions, 24 | } from "./middlewares/x-dns-prefetch-control/index.js"; 25 | import xDownloadOptions from "./middlewares/x-download-options/index.js"; 26 | import xFrameOptions, { 27 | type XFrameOptionsOptions, 28 | } from "./middlewares/x-frame-options/index.js"; 29 | import xPermittedCrossDomainPolicies, { 30 | type XPermittedCrossDomainPoliciesOptions, 31 | } from "./middlewares/x-permitted-cross-domain-policies/index.js"; 32 | import xPoweredBy from "./middlewares/x-powered-by/index.js"; 33 | import xXssProtection from "./middlewares/x-xss-protection/index.js"; 34 | 35 | export type HelmetOptions = { 36 | contentSecurityPolicy?: ContentSecurityPolicyOptions | boolean; 37 | crossOriginEmbedderPolicy?: CrossOriginEmbedderPolicyOptions | boolean; 38 | crossOriginOpenerPolicy?: CrossOriginOpenerPolicyOptions | boolean; 39 | crossOriginResourcePolicy?: CrossOriginResourcePolicyOptions | boolean; 40 | originAgentCluster?: boolean; 41 | referrerPolicy?: ReferrerPolicyOptions | boolean; 42 | } & ( 43 | | { 44 | strictTransportSecurity?: StrictTransportSecurityOptions | boolean; 45 | hsts?: never; 46 | } 47 | | { 48 | hsts?: StrictTransportSecurityOptions | boolean; 49 | strictTransportSecurity?: never; 50 | } 51 | ) & 52 | ( 53 | | { xContentTypeOptions?: boolean; noSniff?: never } 54 | | { noSniff?: boolean; xContentTypeOptions?: never } 55 | ) & 56 | ( 57 | | { 58 | xDnsPrefetchControl?: XDnsPrefetchControlOptions | boolean; 59 | dnsPrefetchControl?: never; 60 | } 61 | | { 62 | dnsPrefetchControl?: XDnsPrefetchControlOptions | boolean; 63 | xDnsPrefetchControl?: never; 64 | } 65 | ) & 66 | ( 67 | | { xDownloadOptions?: boolean; ieNoOpen?: never } 68 | | { ieNoOpen?: boolean; xDownloadOptions?: never } 69 | ) & 70 | ( 71 | | { xFrameOptions?: XFrameOptionsOptions | boolean; frameguard?: never } 72 | | { frameguard?: XFrameOptionsOptions | boolean; xFrameOptions?: never } 73 | ) & 74 | ( 75 | | { 76 | xPermittedCrossDomainPolicies?: 77 | | XPermittedCrossDomainPoliciesOptions 78 | | boolean; 79 | permittedCrossDomainPolicies?: never; 80 | } 81 | | { 82 | permittedCrossDomainPolicies?: 83 | | XPermittedCrossDomainPoliciesOptions 84 | | boolean; 85 | xPermittedCrossDomainPolicies?: never; 86 | } 87 | ) & 88 | ( 89 | | { xPoweredBy?: boolean; hidePoweredBy?: never } 90 | | { hidePoweredBy?: boolean; xPoweredBy?: never } 91 | ) & 92 | ( 93 | | { xXssProtection?: boolean; xssFilter?: never } 94 | | { xssFilter?: boolean; xXssProtection?: never } 95 | ); 96 | 97 | type MiddlewareFunction = ( 98 | req: IncomingMessage, 99 | res: ServerResponse, 100 | next: (error?: Error) => void, 101 | ) => void; 102 | 103 | interface Helmet { 104 | ( 105 | options?: Readonly, 106 | ): ( 107 | req: IncomingMessage, 108 | res: ServerResponse, 109 | next: (err?: unknown) => void, 110 | ) => void; 111 | 112 | contentSecurityPolicy: typeof contentSecurityPolicy; 113 | crossOriginEmbedderPolicy: typeof crossOriginEmbedderPolicy; 114 | crossOriginOpenerPolicy: typeof crossOriginOpenerPolicy; 115 | crossOriginResourcePolicy: typeof crossOriginResourcePolicy; 116 | originAgentCluster: typeof originAgentCluster; 117 | referrerPolicy: typeof referrerPolicy; 118 | strictTransportSecurity: typeof strictTransportSecurity; 119 | xContentTypeOptions: typeof xContentTypeOptions; 120 | xDnsPrefetchControl: typeof xDnsPrefetchControl; 121 | xDownloadOptions: typeof xDownloadOptions; 122 | xFrameOptions: typeof xFrameOptions; 123 | xPermittedCrossDomainPolicies: typeof xPermittedCrossDomainPolicies; 124 | xPoweredBy: typeof xPoweredBy; 125 | xXssProtection: typeof xXssProtection; 126 | 127 | // Legacy aliases 128 | dnsPrefetchControl: typeof xDnsPrefetchControl; 129 | frameguard: typeof xFrameOptions; 130 | hidePoweredBy: typeof xPoweredBy; 131 | hsts: typeof strictTransportSecurity; 132 | ieNoOpen: typeof xDownloadOptions; 133 | noSniff: typeof xContentTypeOptions; 134 | permittedCrossDomainPolicies: typeof xPermittedCrossDomainPolicies; 135 | xssFilter: typeof xXssProtection; 136 | } 137 | 138 | function getMiddlewareFunctionsFromOptions( 139 | options: Readonly, 140 | ): MiddlewareFunction[] { 141 | const result: MiddlewareFunction[] = []; 142 | 143 | switch (options.contentSecurityPolicy) { 144 | case undefined: 145 | case true: 146 | result.push(contentSecurityPolicy()); 147 | break; 148 | case false: 149 | break; 150 | default: 151 | result.push(contentSecurityPolicy(options.contentSecurityPolicy)); 152 | break; 153 | } 154 | 155 | switch (options.crossOriginEmbedderPolicy) { 156 | case undefined: 157 | case false: 158 | break; 159 | case true: 160 | result.push(crossOriginEmbedderPolicy()); 161 | break; 162 | default: 163 | result.push(crossOriginEmbedderPolicy(options.crossOriginEmbedderPolicy)); 164 | break; 165 | } 166 | 167 | switch (options.crossOriginOpenerPolicy) { 168 | case undefined: 169 | case true: 170 | result.push(crossOriginOpenerPolicy()); 171 | break; 172 | case false: 173 | break; 174 | default: 175 | result.push(crossOriginOpenerPolicy(options.crossOriginOpenerPolicy)); 176 | break; 177 | } 178 | 179 | switch (options.crossOriginResourcePolicy) { 180 | case undefined: 181 | case true: 182 | result.push(crossOriginResourcePolicy()); 183 | break; 184 | case false: 185 | break; 186 | default: 187 | result.push(crossOriginResourcePolicy(options.crossOriginResourcePolicy)); 188 | break; 189 | } 190 | 191 | switch (options.originAgentCluster) { 192 | case undefined: 193 | case true: 194 | result.push(originAgentCluster()); 195 | break; 196 | case false: 197 | break; 198 | default: 199 | console.warn( 200 | "Origin-Agent-Cluster does not take options. Remove the property to silence this warning.", 201 | ); 202 | result.push(originAgentCluster()); 203 | break; 204 | } 205 | 206 | switch (options.referrerPolicy) { 207 | case undefined: 208 | case true: 209 | result.push(referrerPolicy()); 210 | break; 211 | case false: 212 | break; 213 | default: 214 | result.push(referrerPolicy(options.referrerPolicy)); 215 | break; 216 | } 217 | 218 | if ("strictTransportSecurity" in options && "hsts" in options) { 219 | throw new Error( 220 | "Strict-Transport-Security option was specified twice. Remove `hsts` to silence this warning.", 221 | ); 222 | } 223 | const strictTransportSecurityOption = 224 | options.strictTransportSecurity ?? options.hsts; 225 | switch (strictTransportSecurityOption) { 226 | case undefined: 227 | case true: 228 | result.push(strictTransportSecurity()); 229 | break; 230 | case false: 231 | break; 232 | default: 233 | result.push(strictTransportSecurity(strictTransportSecurityOption)); 234 | break; 235 | } 236 | 237 | if ("xContentTypeOptions" in options && "noSniff" in options) { 238 | throw new Error( 239 | "X-Content-Type-Options option was specified twice. Remove `noSniff` to silence this warning.", 240 | ); 241 | } 242 | const xContentTypeOptionsOption = 243 | options.xContentTypeOptions ?? options.noSniff; 244 | switch (xContentTypeOptionsOption) { 245 | case undefined: 246 | case true: 247 | result.push(xContentTypeOptions()); 248 | break; 249 | case false: 250 | break; 251 | default: 252 | console.warn( 253 | "X-Content-Type-Options does not take options. Remove the property to silence this warning.", 254 | ); 255 | result.push(xContentTypeOptions()); 256 | break; 257 | } 258 | 259 | if ("xDnsPrefetchControl" in options && "dnsPrefetchControl" in options) { 260 | throw new Error( 261 | "X-DNS-Prefetch-Control option was specified twice. Remove `dnsPrefetchControl` to silence this warning.", 262 | ); 263 | } 264 | const xDnsPrefetchControlOption = 265 | options.xDnsPrefetchControl ?? options.dnsPrefetchControl; 266 | switch (xDnsPrefetchControlOption) { 267 | case undefined: 268 | case true: 269 | result.push(xDnsPrefetchControl()); 270 | break; 271 | case false: 272 | break; 273 | default: 274 | result.push(xDnsPrefetchControl(xDnsPrefetchControlOption)); 275 | break; 276 | } 277 | 278 | if ("xDownloadOptions" in options && "ieNoOpen" in options) { 279 | throw new Error( 280 | "X-Download-Options option was specified twice. Remove `ieNoOpen` to silence this warning.", 281 | ); 282 | } 283 | const xDownloadOptionsOption = options.xDownloadOptions ?? options.ieNoOpen; 284 | switch (xDownloadOptionsOption) { 285 | case undefined: 286 | case true: 287 | result.push(xDownloadOptions()); 288 | break; 289 | case false: 290 | break; 291 | default: 292 | console.warn( 293 | "X-Download-Options does not take options. Remove the property to silence this warning.", 294 | ); 295 | result.push(xDownloadOptions()); 296 | break; 297 | } 298 | 299 | if ("xFrameOptions" in options && "frameguard" in options) { 300 | throw new Error( 301 | "X-Frame-Options option was specified twice. Remove `frameguard` to silence this warning.", 302 | ); 303 | } 304 | const xFrameOptionsOption = options.xFrameOptions ?? options.frameguard; 305 | switch (xFrameOptionsOption) { 306 | case undefined: 307 | case true: 308 | result.push(xFrameOptions()); 309 | break; 310 | case false: 311 | break; 312 | default: 313 | result.push(xFrameOptions(xFrameOptionsOption)); 314 | break; 315 | } 316 | 317 | if ( 318 | "xPermittedCrossDomainPolicies" in options && 319 | "permittedCrossDomainPolicies" in options 320 | ) { 321 | throw new Error( 322 | "X-Permitted-Cross-Domain-Policies option was specified twice. Remove `permittedCrossDomainPolicies` to silence this warning.", 323 | ); 324 | } 325 | const xPermittedCrossDomainPoliciesOption = 326 | options.xPermittedCrossDomainPolicies ?? 327 | options.permittedCrossDomainPolicies; 328 | switch (xPermittedCrossDomainPoliciesOption) { 329 | case undefined: 330 | case true: 331 | result.push(xPermittedCrossDomainPolicies()); 332 | break; 333 | case false: 334 | break; 335 | default: 336 | result.push( 337 | xPermittedCrossDomainPolicies(xPermittedCrossDomainPoliciesOption), 338 | ); 339 | break; 340 | } 341 | 342 | if ("xPoweredBy" in options && "hidePoweredBy" in options) { 343 | throw new Error( 344 | "X-Powered-By option was specified twice. Remove `hidePoweredBy` to silence this warning.", 345 | ); 346 | } 347 | const xPoweredByOption = options.xPoweredBy ?? options.hidePoweredBy; 348 | switch (xPoweredByOption) { 349 | case undefined: 350 | case true: 351 | result.push(xPoweredBy()); 352 | break; 353 | case false: 354 | break; 355 | default: 356 | console.warn( 357 | "X-Powered-By does not take options. Remove the property to silence this warning.", 358 | ); 359 | result.push(xPoweredBy()); 360 | break; 361 | } 362 | 363 | if ("xXssProtection" in options && "xssFilter" in options) { 364 | throw new Error( 365 | "X-XSS-Protection option was specified twice. Remove `xssFilter` to silence this warning.", 366 | ); 367 | } 368 | const xXssProtectionOption = options.xXssProtection ?? options.xssFilter; 369 | switch (xXssProtectionOption) { 370 | case undefined: 371 | case true: 372 | result.push(xXssProtection()); 373 | break; 374 | case false: 375 | break; 376 | default: 377 | console.warn( 378 | "X-XSS-Protection does not take options. Remove the property to silence this warning.", 379 | ); 380 | result.push(xXssProtection()); 381 | break; 382 | } 383 | 384 | return result; 385 | } 386 | 387 | const helmet: Helmet = Object.assign( 388 | function helmet(options: Readonly = {}) { 389 | // People should be able to pass an options object with no prototype, 390 | // so we want this optional chaining. 391 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 392 | if (options.constructor?.name === "IncomingMessage") { 393 | throw new Error( 394 | "It appears you have done something like `app.use(helmet)`, but it should be `app.use(helmet())`.", 395 | ); 396 | } 397 | 398 | const middlewareFunctions = getMiddlewareFunctionsFromOptions(options); 399 | 400 | return function helmetMiddleware( 401 | req: IncomingMessage, 402 | res: ServerResponse, 403 | next: (err?: unknown) => void, 404 | ): void { 405 | let middlewareIndex = 0; 406 | 407 | (function internalNext(err?: unknown) { 408 | if (err) { 409 | next(err); 410 | return; 411 | } 412 | 413 | const middlewareFunction = middlewareFunctions[middlewareIndex]; 414 | if (middlewareFunction) { 415 | middlewareIndex++; 416 | middlewareFunction(req, res, internalNext); 417 | } else { 418 | next(); 419 | } 420 | })(); 421 | }; 422 | }, 423 | { 424 | contentSecurityPolicy, 425 | crossOriginEmbedderPolicy, 426 | crossOriginOpenerPolicy, 427 | crossOriginResourcePolicy, 428 | originAgentCluster, 429 | referrerPolicy, 430 | strictTransportSecurity, 431 | xContentTypeOptions, 432 | xDnsPrefetchControl, 433 | xDownloadOptions, 434 | xFrameOptions, 435 | xPermittedCrossDomainPolicies, 436 | xPoweredBy, 437 | xXssProtection, 438 | 439 | // Legacy aliases 440 | dnsPrefetchControl: xDnsPrefetchControl, 441 | xssFilter: xXssProtection, 442 | permittedCrossDomainPolicies: xPermittedCrossDomainPolicies, 443 | ieNoOpen: xDownloadOptions, 444 | noSniff: xContentTypeOptions, 445 | frameguard: xFrameOptions, 446 | hidePoweredBy: xPoweredBy, 447 | hsts: strictTransportSecurity, 448 | }, 449 | ); 450 | 451 | export default helmet; 452 | 453 | export { 454 | contentSecurityPolicy, 455 | crossOriginEmbedderPolicy, 456 | crossOriginOpenerPolicy, 457 | crossOriginResourcePolicy, 458 | originAgentCluster, 459 | referrerPolicy, 460 | strictTransportSecurity, 461 | xContentTypeOptions, 462 | xDnsPrefetchControl, 463 | xDownloadOptions, 464 | xFrameOptions, 465 | xPoweredBy, 466 | xXssProtection, 467 | 468 | // Legacy aliases 469 | strictTransportSecurity as hsts, 470 | xContentTypeOptions as noSniff, 471 | xDnsPrefetchControl as dnsPrefetchControl, 472 | xDownloadOptions as ieNoOpen, 473 | xFrameOptions as frameguard, 474 | xPermittedCrossDomainPolicies, 475 | xPermittedCrossDomainPolicies as permittedCrossDomainPolicies, 476 | xPoweredBy as hidePoweredBy, 477 | xXssProtection as xssFilter, 478 | }; 479 | -------------------------------------------------------------------------------- /middlewares/content-security-policy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.0.0 - 2024-06-01 4 | 5 | ### Changed 6 | 7 | - **Breaking:** `useDefaults` option now defaults to `true` 8 | - **Breaking:** `form-action` directive is now set to `'self'` by default 9 | - **Breaking:** `block-all-mixed-content` is no longer set by default 10 | 11 | ### Removed 12 | 13 | - **Breaking:** Node 18+ is now required 14 | 15 | ## 3.4.0 - 2021-05-02 16 | 17 | ### Added 18 | 19 | - New `useDefaults` option, defaulting to `false`, lets you selectively override defaults more easily 20 | 21 | ## 3.3.1 - 2020-12-27 22 | 23 | ### Fixed 24 | 25 | - Broken TypeScript types. See [#283](https://github.com/helmetjs/helmet/issues/283) 26 | 27 | ## 3.3.0 - 2020-12-27 28 | 29 | ### Added 30 | 31 | - Setting the `default-src` to `contentSecurityPolicy.dangerouslyDisableDefaultSrc` disables it 32 | 33 | ## 3.2.0 - 2020-11-01 34 | 35 | ### Added 36 | 37 | - Get the default directives with `contentSecurityPolicy.getDefaultDirectives()` 38 | 39 | ## 3.1.0 - 2020-08-15 40 | 41 | ### Added 42 | 43 | - Directive values can now include functions, as they could in Helmet 3. See [#243](https://github.com/helmetjs/helmet/issues/243) 44 | 45 | ## 3.0.0 - 2020-08-02 46 | 47 | ### Added 48 | 49 | - If no `default-src` directive is supplied, an error is thrown 50 | - Directive lists can be any iterable, not just arrays 51 | 52 | ### Changed 53 | 54 | - There is now a default set of directives if none are supplied 55 | - Duplicate keys now throw an error. See [helmetjs/csp#73](https://github.com/helmetjs/csp/issues/73) 56 | - This middleware is more lenient, allowing more directive names or values 57 | 58 | ### Removed 59 | 60 | - Removed browser sniffing (including the `browserSniff` parameter). See [#97](https://github.com/helmetjs/csp/issues/97) 61 | - Removed conditional support. This includes directive functions and support for a function as the `reportOnly`. [Read this if you need help.](https://github.com/helmetjs/helmet/wiki/Conditionally-using-middleware) 62 | - Removed a lot of checks—you should be checking your CSP with a different tool 63 | - Removed support for legacy headers (and therefore the `setAllHeaders` parameter). [Read this if you need help.](https://github.com/helmetjs/helmet/wiki/Setting-legacy-Content-Security-Policy-headers-in-Helmet-4) 64 | - Dropped support for old Node versions. Node 10+ is now required 65 | - Removed the `loose` option 66 | - Removed support for functions as directive values. You must supply an iterable of strings 67 | - Removed the `disableAndroid` option 68 | 69 | ## 2.9.5 - 2020-02-22 70 | 71 | ### Changed 72 | 73 | - Updated `bowser` subdependency from 2.7.0 to 2.9.0 74 | 75 | ### Fixed 76 | 77 | - Fixed an issue some people were having when importing the `bowser` subdependency. See [#96](https://github.com/helmetjs/csp/issues/96) and [#101](https://github.com/helmetjs/csp/pull/101) 78 | - Fixed a link in the readme. See [#100](https://github.com/helmetjs/csp/pull/100) 79 | 80 | ## 2.9.4 - 2019-10-21 81 | 82 | ### Changed 83 | 84 | - Updated `bowser` subdependency from 2.6.1 to 2.7.0. See [#94](https://github.com/helmetjs/csp/pull/94) 85 | 86 | ## 2.9.3 - 2019-09-30 87 | 88 | ### Fixed 89 | 90 | - Published a missing TypeScript type definition file. See [#90](https://github.com/helmetjs/csp/issues/90) 91 | 92 | ## 2.9.2 - 2019-09-20 93 | 94 | ### Fixed 95 | 96 | - Fixed a bug where a request from Firefox 4 could delete `default-src` from future responses 97 | - Fixed tablet PC detection by updating `bowser` subdependency to latest version 98 | 99 | ## 2.9.1 - 2019-09-04 100 | 101 | ### Changed 102 | 103 | - Updated `bowser` subdependency from 2.5.3 to 2.5.4. See [#88](https://github.com/helmetjs/csp/pull/88) 104 | 105 | ### Fixed 106 | 107 | - The "security" keyword was declared twice in package metadata. See [#87](https://github.com/helmetjs/csp/pull/87) 108 | 109 | ## 2.9.0 - 2019-08-28 110 | 111 | ### Added 112 | 113 | - Added TypeScript type definitions. See [#86](https://github.com/helmetjs/csp/pull/86) 114 | 115 | ### Fixed 116 | 117 | - Switched from `platform` to `bowser` to quiet a security vulnerability warning. See [#80](https://github.com/helmetjs/csp/issues/80) 118 | 119 | ## 2.8.0 - 2019-07-24 120 | 121 | ### Added 122 | 123 | - Added a new `sandbox` directive, `allow-downloads-without-user-activation` (see [#85](https://github.com/helmetjs/csp/pull/85)) 124 | - Created a changelog 125 | - Added some package metadata 126 | 127 | ### Changed 128 | 129 | - Updated documentation to use ES2015 130 | - Updated documentation to remove dependency on UUID package 131 | - Updated `content-security-policy-builder` to 2.1.0 132 | - Excluded some files from the npm package 133 | 134 | Changes in versions 2.7.1 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 135 | -------------------------------------------------------------------------------- /middlewares/content-security-policy/README.md: -------------------------------------------------------------------------------- 1 | # Content Security Policy middleware 2 | 3 | The `Content-Security-Policy` header mitigates a large number of attacks, such as [cross-site scripting][XSS]. See [MDN's introductory article on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). 4 | 5 | This header is powerful but likely requires some configuration for your specific app. 6 | 7 | To configure this header, pass an object with a nested `directives` object. Each key is a directive name in camel case (such as `defaultSrc`) or kebab case (such as `default-src`). Each value is an array (or other iterable) of strings or functions for that directive. If a function appears in the array, it will be called with the request and response objects. 8 | 9 | ```javascript 10 | const contentSecurityPolicy = require("helmet-csp"); 11 | 12 | // Sets all of the defaults, but overrides `script-src` 13 | // and disables the default `style-src`. 14 | app.use( 15 | contentSecurityPolicy({ 16 | directives: { 17 | "script-src": ["'self'", "example.com"], 18 | "style-src": null, 19 | }, 20 | }), 21 | ); 22 | ``` 23 | 24 | ```js 25 | // Sets the `script-src` directive to 26 | // "'self' 'nonce-e33cc...'" 27 | // (or similar) 28 | app.use((req, res, next) => { 29 | res.locals.cspNonce = crypto.randomBytes(32).toString("hex"); 30 | next(); 31 | }); 32 | app.use( 33 | contentSecurityPolicy({ 34 | directives: { 35 | scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`], 36 | }, 37 | }), 38 | ); 39 | ``` 40 | 41 | These directives are merged into a default policy, which you can disable by setting `useDefaults` to `false`. 42 | 43 | ```javascript 44 | // Sets "Content-Security-Policy: default-src 'self'; 45 | // script-src 'self' example.com;object-src 'none'; 46 | // upgrade-insecure-requests" 47 | app.use( 48 | contentSecurityPolicy({ 49 | useDefaults: false, 50 | directives: { 51 | defaultSrc: ["'self'"], 52 | scriptSrc: ["'self'", "example.com"], 53 | objectSrc: ["'none'"], 54 | upgradeInsecureRequests: [], 55 | }, 56 | }), 57 | ); 58 | ``` 59 | 60 | You can get the default directives object with `contentSecurityPolicy.getDefaultDirectives()`. Here is the default policy (formatted for readability): 61 | 62 | ``` 63 | default-src 'self'; 64 | base-uri 'self'; 65 | font-src 'self' https: data:; 66 | form-action 'self'; 67 | frame-ancestors 'self'; 68 | img-src 'self' data:; 69 | object-src 'none'; 70 | script-src 'self'; 71 | script-src-attr 'none'; 72 | style-src 'self' https: 'unsafe-inline'; 73 | upgrade-insecure-requests 74 | ``` 75 | 76 | The `default-src` directive can be explicitly disabled by setting its value to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`, but this is not recommended. 77 | 78 | You can set the [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) instead: 79 | 80 | ```javascript 81 | // Sets the Content-Security-Policy-Report-Only header 82 | app.use( 83 | contentSecurityPolicy({ 84 | directives: { 85 | /* ... */ 86 | }, 87 | reportOnly: true, 88 | }), 89 | ); 90 | ``` 91 | 92 | This module performs very little validation on your CSP. You should rely on CSP checkers like [CSP Evaluator](https://csp-evaluator.withgoogle.com/) instead. 93 | -------------------------------------------------------------------------------- /middlewares/content-security-policy/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | type ContentSecurityPolicyDirectiveValueFunction = ( 4 | req: IncomingMessage, 5 | res: ServerResponse, 6 | ) => string; 7 | 8 | type ContentSecurityPolicyDirectiveValue = 9 | | string 10 | | ContentSecurityPolicyDirectiveValueFunction; 11 | 12 | export interface ContentSecurityPolicyOptions { 13 | useDefaults?: boolean; 14 | directives?: Record< 15 | string, 16 | | null 17 | | Iterable 18 | | typeof dangerouslyDisableDefaultSrc 19 | >; 20 | reportOnly?: boolean; 21 | } 22 | 23 | type NormalizedDirectives = Map< 24 | string, 25 | Iterable 26 | >; 27 | 28 | interface ContentSecurityPolicy { 29 | ( 30 | options?: Readonly, 31 | ): ( 32 | req: IncomingMessage, 33 | res: ServerResponse, 34 | next: (err?: Error) => void, 35 | ) => void; 36 | getDefaultDirectives: typeof getDefaultDirectives; 37 | dangerouslyDisableDefaultSrc: typeof dangerouslyDisableDefaultSrc; 38 | } 39 | 40 | const dangerouslyDisableDefaultSrc = Symbol("dangerouslyDisableDefaultSrc"); 41 | 42 | const SHOULD_BE_QUOTED: ReadonlySet = new Set([ 43 | "none", 44 | "self", 45 | "strict-dynamic", 46 | "report-sample", 47 | "inline-speculation-rules", 48 | "unsafe-inline", 49 | "unsafe-eval", 50 | "unsafe-hashes", 51 | "wasm-unsafe-eval", 52 | ]); 53 | 54 | const getDefaultDirectives = (): Record< 55 | string, 56 | Iterable 57 | > => ({ 58 | "default-src": ["'self'"], 59 | "base-uri": ["'self'"], 60 | "font-src": ["'self'", "https:", "data:"], 61 | "form-action": ["'self'"], 62 | "frame-ancestors": ["'self'"], 63 | "img-src": ["'self'", "data:"], 64 | "object-src": ["'none'"], 65 | "script-src": ["'self'"], 66 | "script-src-attr": ["'none'"], 67 | "style-src": ["'self'", "https:", "'unsafe-inline'"], 68 | "upgrade-insecure-requests": [], 69 | }); 70 | 71 | const dashify = (str: string): string => 72 | str.replace(/[A-Z]/g, (capitalLetter) => "-" + capitalLetter.toLowerCase()); 73 | 74 | const assertDirectiveValueIsValid = ( 75 | directiveName: string, 76 | directiveValue: string, 77 | ): void => { 78 | if (/;|,/.test(directiveValue)) { 79 | throw new Error( 80 | `Content-Security-Policy received an invalid directive value for ${JSON.stringify( 81 | directiveName, 82 | )}`, 83 | ); 84 | } 85 | }; 86 | 87 | const assertDirectiveValueEntryIsValid = ( 88 | directiveName: string, 89 | directiveValueEntry: string, 90 | ): void => { 91 | if ( 92 | SHOULD_BE_QUOTED.has(directiveValueEntry) || 93 | directiveValueEntry.startsWith("nonce-") || 94 | directiveValueEntry.startsWith("sha256-") || 95 | directiveValueEntry.startsWith("sha384-") || 96 | directiveValueEntry.startsWith("sha512-") 97 | ) { 98 | throw new Error( 99 | `Content-Security-Policy received an invalid directive value for ${JSON.stringify( 100 | directiveName, 101 | )}. ${JSON.stringify(directiveValueEntry)} should be quoted`, 102 | ); 103 | } 104 | }; 105 | 106 | function normalizeDirectives( 107 | options: Readonly, 108 | ): NormalizedDirectives { 109 | const defaultDirectives = getDefaultDirectives(); 110 | 111 | const { useDefaults = true, directives: rawDirectives = defaultDirectives } = 112 | options; 113 | 114 | const result: NormalizedDirectives = new Map(); 115 | const directiveNamesSeen = new Set(); 116 | const directivesExplicitlyDisabled = new Set(); 117 | 118 | for (const rawDirectiveName in rawDirectives) { 119 | if (!Object.hasOwn(rawDirectives, rawDirectiveName)) { 120 | continue; 121 | } 122 | 123 | if ( 124 | rawDirectiveName.length === 0 || 125 | /[^a-zA-Z0-9-]/.test(rawDirectiveName) 126 | ) { 127 | throw new Error( 128 | `Content-Security-Policy received an invalid directive name ${JSON.stringify( 129 | rawDirectiveName, 130 | )}`, 131 | ); 132 | } 133 | 134 | const directiveName = dashify(rawDirectiveName); 135 | 136 | if (directiveNamesSeen.has(directiveName)) { 137 | throw new Error( 138 | `Content-Security-Policy received a duplicate directive ${JSON.stringify( 139 | directiveName, 140 | )}`, 141 | ); 142 | } 143 | directiveNamesSeen.add(directiveName); 144 | 145 | const rawDirectiveValue = rawDirectives[rawDirectiveName]; 146 | let directiveValue: Iterable; 147 | if (rawDirectiveValue === null) { 148 | if (directiveName === "default-src") { 149 | throw new Error( 150 | "Content-Security-Policy needs a default-src but it was set to `null`. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.", 151 | ); 152 | } 153 | directivesExplicitlyDisabled.add(directiveName); 154 | continue; 155 | } else if (typeof rawDirectiveValue === "string") { 156 | directiveValue = [rawDirectiveValue]; 157 | } else if (!rawDirectiveValue) { 158 | throw new Error( 159 | `Content-Security-Policy received an invalid directive value for ${JSON.stringify( 160 | directiveName, 161 | )}`, 162 | ); 163 | } else if (rawDirectiveValue === dangerouslyDisableDefaultSrc) { 164 | if (directiveName === "default-src") { 165 | directivesExplicitlyDisabled.add("default-src"); 166 | continue; 167 | } else { 168 | throw new Error( 169 | `Content-Security-Policy: tried to disable ${JSON.stringify( 170 | directiveName, 171 | )} as if it were default-src; simply omit the key`, 172 | ); 173 | } 174 | } else { 175 | directiveValue = rawDirectiveValue; 176 | } 177 | 178 | for (const element of directiveValue) { 179 | if (typeof element !== "string") continue; 180 | assertDirectiveValueIsValid(directiveName, element); 181 | assertDirectiveValueEntryIsValid(directiveName, element); 182 | } 183 | 184 | result.set(directiveName, directiveValue); 185 | } 186 | 187 | if (useDefaults) { 188 | Object.entries(defaultDirectives).forEach( 189 | ([defaultDirectiveName, defaultDirectiveValue]) => { 190 | if ( 191 | !result.has(defaultDirectiveName) && 192 | !directivesExplicitlyDisabled.has(defaultDirectiveName) 193 | ) { 194 | result.set(defaultDirectiveName, defaultDirectiveValue); 195 | } 196 | }, 197 | ); 198 | } 199 | 200 | if (!result.size) { 201 | throw new Error( 202 | "Content-Security-Policy has no directives. Either set some or disable the header", 203 | ); 204 | } 205 | if ( 206 | !result.has("default-src") && 207 | !directivesExplicitlyDisabled.has("default-src") 208 | ) { 209 | throw new Error( 210 | "Content-Security-Policy needs a default-src but none was provided. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.", 211 | ); 212 | } 213 | 214 | return result; 215 | } 216 | 217 | function getHeaderValue( 218 | req: IncomingMessage, 219 | res: ServerResponse, 220 | normalizedDirectives: Readonly, 221 | ): string | Error { 222 | const result: string[] = []; 223 | 224 | for (const [directiveName, rawDirectiveValue] of normalizedDirectives) { 225 | let directiveValue = ""; 226 | for (const element of rawDirectiveValue) { 227 | if (typeof element === "function") { 228 | const newElement = element(req, res); 229 | assertDirectiveValueEntryIsValid(directiveName, newElement); 230 | directiveValue += " " + newElement; 231 | } else { 232 | directiveValue += " " + element; 233 | } 234 | } 235 | 236 | if (directiveValue) { 237 | assertDirectiveValueIsValid(directiveName, directiveValue); 238 | result.push(`${directiveName}${directiveValue}`); 239 | } else { 240 | result.push(directiveName); 241 | } 242 | } 243 | 244 | return result.join(";"); 245 | } 246 | 247 | const contentSecurityPolicy: ContentSecurityPolicy = 248 | function contentSecurityPolicy( 249 | options: Readonly = {}, 250 | ): ( 251 | req: IncomingMessage, 252 | res: ServerResponse, 253 | next: (err?: Error) => void, 254 | ) => void { 255 | const headerName = options.reportOnly 256 | ? "Content-Security-Policy-Report-Only" 257 | : "Content-Security-Policy"; 258 | 259 | const normalizedDirectives = normalizeDirectives(options); 260 | 261 | return function contentSecurityPolicyMiddleware( 262 | req: IncomingMessage, 263 | res: ServerResponse, 264 | next: (error?: Error) => void, 265 | ) { 266 | const result = getHeaderValue(req, res, normalizedDirectives); 267 | if (result instanceof Error) { 268 | next(result); 269 | } else { 270 | res.setHeader(headerName, result); 271 | next(); 272 | } 273 | }; 274 | }; 275 | contentSecurityPolicy.getDefaultDirectives = getDefaultDirectives; 276 | contentSecurityPolicy.dangerouslyDisableDefaultSrc = 277 | dangerouslyDisableDefaultSrc; 278 | 279 | export default contentSecurityPolicy; 280 | 281 | export { dangerouslyDisableDefaultSrc, getDefaultDirectives }; 282 | -------------------------------------------------------------------------------- /middlewares/content-security-policy/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helmet-csp", 3 | "author": "Adam Baldwin (https://evilpacket.net)", 4 | "contributors": [ 5 | "Evan Hahn (https://evanhahn.com)", 6 | "Ryan Cannon (https://ryancannon.com)" 7 | ], 8 | "description": "Content Security Policy middleware", 9 | "version": "4.0.0", 10 | "keywords": ["express", "security", "content-security-policy", "csp", "xss"], 11 | "engines": { 12 | "node": ">=18.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /middlewares/cross-origin-embedder-policy/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface CrossOriginEmbedderPolicyOptions { 4 | policy?: "require-corp" | "credentialless" | "unsafe-none"; 5 | } 6 | 7 | const ALLOWED_POLICIES = new Set([ 8 | "require-corp", 9 | "credentialless", 10 | "unsafe-none", 11 | ]); 12 | 13 | function getHeaderValueFromOptions({ 14 | policy = "require-corp", 15 | }: Readonly): string { 16 | if (ALLOWED_POLICIES.has(policy)) { 17 | return policy; 18 | } else { 19 | throw new Error( 20 | `Cross-Origin-Embedder-Policy does not support the ${JSON.stringify( 21 | policy, 22 | )} policy`, 23 | ); 24 | } 25 | } 26 | 27 | function crossOriginEmbedderPolicy( 28 | options: Readonly = {}, 29 | ) { 30 | const headerValue = getHeaderValueFromOptions(options); 31 | 32 | return function crossOriginEmbedderPolicyMiddleware( 33 | _req: IncomingMessage, 34 | res: ServerResponse, 35 | next: () => void, 36 | ) { 37 | res.setHeader("Cross-Origin-Embedder-Policy", headerValue); 38 | next(); 39 | }; 40 | } 41 | 42 | export default crossOriginEmbedderPolicy; 43 | -------------------------------------------------------------------------------- /middlewares/cross-origin-opener-policy/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface CrossOriginOpenerPolicyOptions { 4 | policy?: "same-origin" | "same-origin-allow-popups" | "unsafe-none"; 5 | } 6 | 7 | const ALLOWED_POLICIES = new Set([ 8 | "same-origin", 9 | "same-origin-allow-popups", 10 | "unsafe-none", 11 | ]); 12 | 13 | function getHeaderValueFromOptions({ 14 | policy = "same-origin", 15 | }: Readonly): string { 16 | if (ALLOWED_POLICIES.has(policy)) { 17 | return policy; 18 | } else { 19 | throw new Error( 20 | `Cross-Origin-Opener-Policy does not support the ${JSON.stringify( 21 | policy, 22 | )} policy`, 23 | ); 24 | } 25 | } 26 | 27 | function crossOriginOpenerPolicy( 28 | options: Readonly = {}, 29 | ) { 30 | const headerValue = getHeaderValueFromOptions(options); 31 | 32 | return function crossOriginOpenerPolicyMiddleware( 33 | _req: IncomingMessage, 34 | res: ServerResponse, 35 | next: () => void, 36 | ) { 37 | res.setHeader("Cross-Origin-Opener-Policy", headerValue); 38 | next(); 39 | }; 40 | } 41 | 42 | export default crossOriginOpenerPolicy; 43 | -------------------------------------------------------------------------------- /middlewares/cross-origin-resource-policy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - **Breaking:** increase TypeScript strictness around arguments. Only affects TypeScript users. See [helmetjs/helmet#369](https://github.com/helmetjs/helmet/issues/369) 8 | 9 | ## 0.3.0 - 2021-04-17 10 | 11 | ### Added 12 | 13 | - Added support for the `cross-origin` policy 14 | 15 | ## 0.2.1 - 2020-12-22 16 | 17 | ### Fixed 18 | 19 | - Fixed incorrect example in README 20 | 21 | ## 0.2.0 - 2019-07-17 22 | 23 | ### Added 24 | 25 | - Added TypeScript type definitions. See [#2](https://github.com/helmetjs/cross-origin-resource-policy/pull/2) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 26 | - Created a changelog 27 | - Added some additional package metadata: homepage, email for bug reports, and a list of supported Node versions 28 | 29 | ### Changed 30 | 31 | - Excluded some files from npm package 32 | 33 | This changelog was started in version 0.2.0. 34 | -------------------------------------------------------------------------------- /middlewares/cross-origin-resource-policy/README.md: -------------------------------------------------------------------------------- 1 | # Cross-Origin-Resource-Policy middleware 2 | 3 | This middleware sets the `Cross-Origin-Resource-Policy` header. Read about it [in the spec](https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header). 4 | 5 | Usage: 6 | 7 | ```javascript 8 | const crossOriginResourcePolicy = require("cross-origin-resource-policy"); 9 | 10 | // Sets "Cross-Origin-Resource-Policy: same-origin" 11 | app.use(crossOriginResourcePolicy({ policy: "same-origin" })); 12 | 13 | // Sets "Cross-Origin-Resource-Policy: same-site" 14 | app.use(crossOriginResourcePolicy({ policy: "same-site" })); 15 | ``` 16 | -------------------------------------------------------------------------------- /middlewares/cross-origin-resource-policy/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface CrossOriginResourcePolicyOptions { 4 | policy?: "same-origin" | "same-site" | "cross-origin"; 5 | } 6 | 7 | const ALLOWED_POLICIES = new Set(["same-origin", "same-site", "cross-origin"]); 8 | 9 | function getHeaderValueFromOptions({ 10 | policy = "same-origin", 11 | }: Readonly): string { 12 | if (ALLOWED_POLICIES.has(policy)) { 13 | return policy; 14 | } else { 15 | throw new Error( 16 | `Cross-Origin-Resource-Policy does not support the ${JSON.stringify( 17 | policy, 18 | )} policy`, 19 | ); 20 | } 21 | } 22 | 23 | function crossOriginResourcePolicy( 24 | options: Readonly = {}, 25 | ) { 26 | const headerValue = getHeaderValueFromOptions(options); 27 | 28 | return function crossOriginResourcePolicyMiddleware( 29 | _req: IncomingMessage, 30 | res: ServerResponse, 31 | next: () => void, 32 | ) { 33 | res.setHeader("Cross-Origin-Resource-Policy", headerValue); 34 | next(); 35 | }; 36 | } 37 | 38 | export default crossOriginResourcePolicy; 39 | -------------------------------------------------------------------------------- /middlewares/cross-origin-resource-policy/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cross-origin-resource-policy", 3 | "author": "Evan Hahn (https://evanhahn.com)", 4 | "contributors": [], 5 | "description": "Middleware to set the Cross-Origin-Resource-Policy header", 6 | "version": "0.3.0", 7 | "keywords": ["cross-origin-resource-policy"], 8 | "engines": { 9 | "node": ">=10.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /middlewares/origin-agent-cluster/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | function originAgentCluster() { 4 | return function originAgentClusterMiddleware( 5 | _req: IncomingMessage, 6 | res: ServerResponse, 7 | next: () => void, 8 | ): void { 9 | res.setHeader("Origin-Agent-Cluster", "?1"); 10 | next(); 11 | }; 12 | } 13 | 14 | export default originAgentCluster; 15 | -------------------------------------------------------------------------------- /middlewares/referrer-policy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - **Breaking:** increase TypeScript strictness around arguments. Only affects TypeScript users. See [helmetjs/helmet#369](https://github.com/helmetjs/helmet/issues/369) 8 | 9 | ## 2.0.0 - Unreleased 10 | 11 | ### Removed 12 | 13 | - **Breaking:** Dropped support for old Node versions. Node 10+ is now required 14 | 15 | ## 1.2.0 - 2019-05-03 16 | 17 | ### Added 18 | 19 | - Allow multiple values to be set. See [#7](https://github.com/helmetjs/referrer-policy/issues/7) 20 | - Added TypeScript type definitions. See [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 21 | - Created a changelog 22 | 23 | ### Changed 24 | 25 | - Updated documentation 26 | 27 | Changes in versions 1.1.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 28 | -------------------------------------------------------------------------------- /middlewares/referrer-policy/README.md: -------------------------------------------------------------------------------- 1 | # Referrer-Policy middleware 2 | 3 | The [Referer HTTP header](https://en.wikipedia.org/wiki/HTTP_referer) is typically set by web browsers to tell the server where it's coming from. For example, if you click a link on _example.com/index.html_ that takes you to _wikipedia.org_, Wikipedia's servers will see `Referer: example.com`. This can have privacy implications—websites can see where you are coming from. The new [`Referrer-Policy` HTTP header](https://www.w3.org/TR/referrer-policy/#referrer-policy-header) lets authors control how browsers set the Referer header. 4 | 5 | [Read the spec](https://www.w3.org/TR/referrer-policy/#referrer-policies) to see the options you can provide. 6 | 7 | Usage: 8 | 9 | ```javascript 10 | const referrerPolicy = require("referrer-policy"); 11 | 12 | app.use(referrerPolicy({ policy: "same-origin" })); 13 | // Referrer-Policy: same-origin 14 | 15 | app.use(referrerPolicy({ policy: "unsafe-url" })); 16 | // Referrer-Policy: unsafe-url 17 | 18 | app.use( 19 | referrerPolicy({ 20 | policy: ["no-referrer", "unsafe-url"], 21 | }), 22 | ); 23 | // Referrer-Policy: no-referrer,unsafe-url 24 | 25 | app.use(referrerPolicy()); 26 | // Referrer-Policy: no-referrer 27 | ``` 28 | -------------------------------------------------------------------------------- /middlewares/referrer-policy/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | type ReferrerPolicyToken = 4 | | "no-referrer" 5 | | "no-referrer-when-downgrade" 6 | | "same-origin" 7 | | "origin" 8 | | "strict-origin" 9 | | "origin-when-cross-origin" 10 | | "strict-origin-when-cross-origin" 11 | | "unsafe-url" 12 | | ""; 13 | 14 | export interface ReferrerPolicyOptions { 15 | policy?: ReferrerPolicyToken | ReferrerPolicyToken[]; 16 | } 17 | 18 | const ALLOWED_TOKENS = new Set([ 19 | "no-referrer", 20 | "no-referrer-when-downgrade", 21 | "same-origin", 22 | "origin", 23 | "strict-origin", 24 | "origin-when-cross-origin", 25 | "strict-origin-when-cross-origin", 26 | "unsafe-url", 27 | "", 28 | ]); 29 | 30 | function getHeaderValueFromOptions({ 31 | policy = ["no-referrer"], 32 | }: Readonly): string { 33 | const tokens = typeof policy === "string" ? [policy] : policy; 34 | 35 | if (tokens.length === 0) { 36 | throw new Error("Referrer-Policy received no policy tokens"); 37 | } 38 | 39 | const tokensSeen = new Set(); 40 | tokens.forEach((token) => { 41 | if (!ALLOWED_TOKENS.has(token)) { 42 | throw new Error( 43 | `Referrer-Policy received an unexpected policy token ${JSON.stringify( 44 | token, 45 | )}`, 46 | ); 47 | } else if (tokensSeen.has(token)) { 48 | throw new Error( 49 | `Referrer-Policy received a duplicate policy token ${JSON.stringify( 50 | token, 51 | )}`, 52 | ); 53 | } 54 | tokensSeen.add(token); 55 | }); 56 | 57 | return tokens.join(","); 58 | } 59 | 60 | function referrerPolicy(options: Readonly = {}) { 61 | const headerValue = getHeaderValueFromOptions(options); 62 | 63 | return function referrerPolicyMiddleware( 64 | _req: IncomingMessage, 65 | res: ServerResponse, 66 | next: () => void, 67 | ) { 68 | res.setHeader("Referrer-Policy", headerValue); 69 | next(); 70 | }; 71 | } 72 | 73 | export default referrerPolicy; 74 | -------------------------------------------------------------------------------- /middlewares/referrer-policy/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "referrer-policy", 3 | "author": "Evan Hahn (https://evanhahn.com)", 4 | "contributors": [], 5 | "description": "Middleware to set the Referrer-Policy HTTP header", 6 | "version": "1.2.0", 7 | "keywords": [ 8 | "express", 9 | "security", 10 | "referer", 11 | "referrer", 12 | "referrer-policy", 13 | "privacy" 14 | ], 15 | "engines": { 16 | "node": ">=10.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /middlewares/strict-transport-security/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.0 - Unreleased 4 | 5 | ### Added 6 | 7 | - TypeScript type definitions. See [#25](https://github.com/helmetjs/hsts/pull/25) 8 | 9 | ### Removed 10 | 11 | - Dropped support for `includeSubdomains` with a lowercase D. See [#231](https://github.com/helmetjs/helmet/issues/231) 12 | - Dropped support for `setIf`. [Read this if you need help.](https://github.com/helmetjs/helmet/wiki/Conditionally-using-middleware). See [#232](https://github.com/helmetjs/helmet/issues/232) 13 | - Dropped support for old Node versions. Node 10+ is now required 14 | 15 | ## 2.2.0 - 2019-03-10 16 | 17 | ### Added 18 | 19 | - Created a changelog 20 | 21 | ### Changed 22 | 23 | - Mark the module as Node 4+ in the `engines` field of `package.json` 24 | - Add a `homepage` in `package.json` 25 | - Add an email to `package.json`'s `bugs` field 26 | - Updated documentation 27 | - Updated Adam Baldwin's contact info. See [helmetjs/helmet#189](https://github.com/helmetjs/helmet/issues/189) 28 | 29 | ### Deprecated 30 | 31 | - The `setIf` option has been deprecated and will be removed in `hsts@3`. Refer to the documentation to see how to do without it. See [#22](https://github.com/helmetjs/hsts/issues/22) for more 32 | - The `includeSubdomains` option (with a lowercase `d`) has been deprecated and will be removed in `hsts@3`. Use the uppercase-D `includeSubDomains` option instead. See [#21](https://github.com/helmetjs/hsts/issues/21) for more 33 | 34 | Changes in versions 2.1.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 35 | -------------------------------------------------------------------------------- /middlewares/strict-transport-security/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Strict Transport Security middleware 2 | 3 | This middleware adds the `Strict-Transport-Security` header to the response. This tells browsers, "hey, only use HTTPS for the next period of time". ([See the spec](https://tools.ietf.org/html/rfc6797) for more.) Note that the header won't tell users on HTTP to _switch_ to HTTPS, it will just tell HTTPS users to stick around. You can enforce HTTPS with the [express-enforces-ssl](https://github.com/aredo/express-enforces-ssl) module. 4 | 5 | This will set the Strict Transport Security header, telling browsers to visit by HTTPS for the next 365 days: 6 | 7 | ```javascript 8 | const strictTransportSecurity = require("hsts"); 9 | 10 | // Sets "Strict-Transport-Security: max-age=31536000; includeSubDomains" 11 | app.use( 12 | strictTransportSecurity({ 13 | maxAge: 31536000, // 365 days in seconds 14 | }), 15 | ); 16 | ``` 17 | 18 | Note that the max age must be in seconds. 19 | 20 | The `includeSubDomains` directive is present by default. If this header is set on _example.com_, supported browsers will also use HTTPS on _my-subdomain.example.com_. You can disable this: 21 | 22 | ```javascript 23 | app.use( 24 | strictTransportSecurity({ 25 | maxAge: 31536000, 26 | includeSubDomains: false, 27 | }), 28 | ); 29 | ``` 30 | 31 | Some browsers let you submit your site's HSTS to be baked into the browser. You can add `preload` to the header with the following code. You can check your eligibility and submit your site at [hstspreload.org](https://hstspreload.org/). 32 | 33 | ```javascript 34 | app.use( 35 | strictTransportSecurity({ 36 | maxAge: 31536000, // Must be at least 1 year to be approved 37 | includeSubDomains: true, // Must be enabled to be approved 38 | preload: true, 39 | }), 40 | ); 41 | ``` 42 | 43 | [The header is ignored in insecure HTTP](https://tools.ietf.org/html/rfc6797#section-8.1), so it's safe to set in development. 44 | 45 | This header is [somewhat well-supported by browsers](https://caniuse.com/#feat=stricttransportsecurity). 46 | -------------------------------------------------------------------------------- /middlewares/strict-transport-security/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; 4 | 5 | export interface StrictTransportSecurityOptions { 6 | maxAge?: number; 7 | includeSubDomains?: boolean; 8 | preload?: boolean; 9 | } 10 | 11 | function parseMaxAge(value: number = DEFAULT_MAX_AGE): number { 12 | if (value >= 0 && Number.isFinite(value)) { 13 | return Math.floor(value); 14 | } else { 15 | throw new Error( 16 | `Strict-Transport-Security: ${JSON.stringify( 17 | value, 18 | )} is not a valid value for maxAge. Please choose a positive integer.`, 19 | ); 20 | } 21 | } 22 | 23 | function getHeaderValueFromOptions( 24 | options: Readonly, 25 | ): string { 26 | if ("maxage" in options) { 27 | throw new Error( 28 | "Strict-Transport-Security received an unsupported property, `maxage`. Did you mean to pass `maxAge`?", 29 | ); 30 | } 31 | if ("includeSubdomains" in options) { 32 | throw new Error( 33 | 'Strict-Transport-Security middleware should use `includeSubDomains` instead of `includeSubdomains`. (The correct one has an uppercase "D".)', 34 | ); 35 | } 36 | 37 | const directives: string[] = [`max-age=${parseMaxAge(options.maxAge)}`]; 38 | 39 | if (options.includeSubDomains === undefined || options.includeSubDomains) { 40 | directives.push("includeSubDomains"); 41 | } 42 | 43 | if (options.preload) { 44 | directives.push("preload"); 45 | } 46 | 47 | return directives.join("; "); 48 | } 49 | 50 | function strictTransportSecurity( 51 | options: Readonly = {}, 52 | ) { 53 | const headerValue = getHeaderValueFromOptions(options); 54 | 55 | return function strictTransportSecurityMiddleware( 56 | _req: IncomingMessage, 57 | res: ServerResponse, 58 | next: () => void, 59 | ) { 60 | res.setHeader("Strict-Transport-Security", headerValue); 61 | next(); 62 | }; 63 | } 64 | 65 | export default strictTransportSecurity; 66 | -------------------------------------------------------------------------------- /middlewares/strict-transport-security/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hsts", 3 | "description": "HTTP Strict Transport Security middleware", 4 | "version": "2.2.0", 5 | "keywords": [ 6 | "express", 7 | "security", 8 | "hsts", 9 | "strict-transport-security", 10 | "https" 11 | ], 12 | "engines": { 13 | "node": ">=10.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /middlewares/x-content-type-options/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 - Unreleased 4 | 5 | ### Removed 6 | 7 | - Dropped support for old Node versions. Node 10+ is now required 8 | 9 | ## 1.1.0 - 2019-05-11 10 | 11 | ### Added 12 | 13 | - Added TypeScript type definitions. See [#4](https://github.com/helmetjs/dont-sniff-mimetype/issues/4) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 14 | - Created a changelog 15 | 16 | ### Changed 17 | 18 | - Updated some package metadata 19 | - Excluded some files from npm package 20 | 21 | Changes in versions 1.0.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 22 | -------------------------------------------------------------------------------- /middlewares/x-content-type-options/README.md: -------------------------------------------------------------------------------- 1 | # X-Content-Type-Options middleware 2 | 3 | Some browsers will try to "sniff" mimetypes. For example, if my server serves _file.txt_ with a _text/plain_ content-type, some browsers can still run that file with ``. Many browsers will allow _file.js_ to be run even if the content-type isn't for JavaScript. 4 | 5 | Browsers' same-origin policies generally prevent remote resources from being loaded dangerously, but vulnerabilities in web browsers can cause this to be abused. Some browsers, like [Chrome](https://developers.google.com/web/updates/2018/07/site-isolation), will further isolate memory if the `X-Content-Type-Options` header is seen. 6 | 7 | There are [some other vulnerabilities](https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/), too. 8 | 9 | This middleware prevents Chrome, Opera 13+, IE 8+ and [Firefox 50+](https://bugzilla.mozilla.org/show_bug.cgi?id=471020) from doing this sniffing. The following example sets the `X-Content-Type-Options` header to its only option, `nosniff`: 10 | 11 | ```javascript 12 | const dontSniffMimetype = require("dont-sniff-mimetype"); 13 | app.use(dontSniffMimetype()); 14 | ``` 15 | 16 | [MSDN has a good description](https://msdn.microsoft.com/en-us/library/gg622941%28v=vs.85%29.aspx) of how browsers behave when this header is sent. 17 | -------------------------------------------------------------------------------- /middlewares/x-content-type-options/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | function xContentTypeOptions() { 4 | return function xContentTypeOptionsMiddleware( 5 | _req: IncomingMessage, 6 | res: ServerResponse, 7 | next: () => void, 8 | ) { 9 | res.setHeader("X-Content-Type-Options", "nosniff"); 10 | next(); 11 | }; 12 | } 13 | 14 | export default xContentTypeOptions; 15 | -------------------------------------------------------------------------------- /middlewares/x-content-type-options/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dont-sniff-mimetype", 3 | "description": "Middleware to prevent mimetype from being sniffed", 4 | "version": "1.1.0", 5 | "keywords": ["express", "security", "mimetype", "x-content-type-options"], 6 | "engines": { 7 | "node": ">=10.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /middlewares/x-dns-prefetch-control/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### Removed 6 | 7 | - Dropped support for old Node versions. Node 10+ is now required 8 | 9 | ## 0.3.0 - 2019-09-01 10 | 11 | ### Changed 12 | 13 | - Dropped support for Node <8 14 | 15 | ## 0.2.0 - 2019-05-11 16 | 17 | ### Added 18 | 19 | - Added TypeScript type definitions. See [#2](https://github.com/helmetjs/dns-prefetch-control/pull/2) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 20 | - Created a changelog 21 | 22 | ### Changed 23 | 24 | - Update some package metadata 25 | - Excluded some files from npm package 26 | 27 | Changes in versions 0.1.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 28 | -------------------------------------------------------------------------------- /middlewares/x-dns-prefetch-control/README.md: -------------------------------------------------------------------------------- 1 | # X-DNS-Prefetch-Control middleware 2 | 3 | This middleware lets you set the `X-DNS-Prefetch-Control` to control browsers' DNS prefetching. Read more about it [on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Controlling_DNS_prefetching) and [on Chromium's docs](https://dev.chromium.org/developers/design-documents/dns-prefetching). 4 | 5 | Usage: 6 | 7 | ```js 8 | const dnsPrefetchControl = require("dns-prefetch-control"); 9 | 10 | // Set X-DNS-Prefetch-Control: off 11 | app.use(dnsPrefetchControl()); 12 | 13 | // Set X-DNS-Prefetch-Control: off 14 | app.use(dnsPrefetchControl({ allow: false })); 15 | 16 | // Set X-DNS-Prefetch-Control: on 17 | app.use(dnsPrefetchControl({ allow: true })); 18 | ``` 19 | -------------------------------------------------------------------------------- /middlewares/x-dns-prefetch-control/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface XDnsPrefetchControlOptions { 4 | allow?: boolean; 5 | } 6 | 7 | function xDnsPrefetchControl( 8 | options: Readonly = {}, 9 | ) { 10 | const headerValue = options.allow ? "on" : "off"; 11 | 12 | return function xDnsPrefetchControlMiddleware( 13 | _req: IncomingMessage, 14 | res: ServerResponse, 15 | next: () => void, 16 | ): void { 17 | res.setHeader("X-DNS-Prefetch-Control", headerValue); 18 | next(); 19 | }; 20 | } 21 | 22 | export default xDnsPrefetchControl; 23 | -------------------------------------------------------------------------------- /middlewares/x-dns-prefetch-control/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dns-prefetch-control", 3 | "author": "Evan Hahn (https://evanhahn.com)", 4 | "contributors": [], 5 | "description": "Middleware to set X-DNS-Prefetch-Control header.", 6 | "version": "0.3.0", 7 | "keywords": ["express", "security", "x-dns-prefetch-control", "prefetch"], 8 | "engines": { 9 | "node": ">=10.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /middlewares/x-download-options/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 - Unreleased 4 | 5 | - Dropped support for old Node versions. Node 10+ is now required 6 | 7 | ## 1.1.1 - 2020-06-16 8 | 9 | ### Changed 10 | 11 | - Excluded more files from npm package 12 | 13 | ## 1.1.0 - 2019-03-10 14 | 15 | ### Added 16 | 17 | - Added TypeScript type definitions. See [#1](https://github.com/helmetjs/ienoopen/pull/1) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 18 | - Created a changelog 19 | 20 | ### Changed 21 | 22 | - Updated documentation 23 | - Excluded some files from npm package 24 | 25 | Changes in versions 1.0.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 26 | -------------------------------------------------------------------------------- /middlewares/x-download-options/README.md: -------------------------------------------------------------------------------- 1 | # X-Download-Options middleware 2 | 3 | This middleware sets the `X-Download-Options` header to `noopen` to prevent Internet Explorer users from executing downloads in your site's context. 4 | 5 | ```javascript 6 | const ienoopen = require("ienoopen"); 7 | app.use(ienoopen()); 8 | ``` 9 | 10 | Some web applications will serve untrusted HTML for download. By default, some versions of IE will allow you to open those HTML files _in the context of your site_, which means that an untrusted HTML page could start doing bad things in the context of your pages. For more, see [this MSDN blog post](https://docs.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection). 11 | 12 | This is pretty obscure, fixing a small bug on IE only. No real drawbacks other than performance/bandwidth of setting the headers, though. 13 | -------------------------------------------------------------------------------- /middlewares/x-download-options/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | function xDownloadOptions() { 4 | return function xDownloadOptionsMiddleware( 5 | _req: IncomingMessage, 6 | res: ServerResponse, 7 | next: () => void, 8 | ): void { 9 | res.setHeader("X-Download-Options", "noopen"); 10 | next(); 11 | }; 12 | } 13 | 14 | export default xDownloadOptions; 15 | -------------------------------------------------------------------------------- /middlewares/x-download-options/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ienoopen", 3 | "contributors": [ 4 | "Evan Hahn (https://evanhahn.com)", 5 | "Nathan Shively-Sanders (https://github.com/sandersn)" 6 | ], 7 | "description": "Middleware to set `X-Download-Options` header for IE8 security", 8 | "version": "1.1.1", 9 | "keywords": ["express", "security", "x-download-options"], 10 | "engines": { 11 | "node": ">=10.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middlewares/x-frame-options/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - **Breaking:** increase TypeScript strictness around arguments. Only affects TypeScript users. See [helmetjs/helmet#369](https://github.com/helmetjs/helmet/issues/369) 8 | - No longer offer a specific error when trying to use `ALLOW-FROM`; it just says that it is unsupported. Only the error message has changed 9 | 10 | ## 4.0.0 - 2020-12-21 11 | 12 | ### Removed 13 | 14 | - Dropped support for the `ALLOW-FROM` action. [Read more here.](https://github.com/helmetjs/helmet/wiki/How-to-use-X%E2%80%93Frame%E2%80%93Options's-%60ALLOW%E2%80%93FROM%60-directive) 15 | - Dropped support for old Node versions. Node 10+ is now required 16 | 17 | ## 3.1.0 - 2019-05-04 18 | 19 | ### Added 20 | 21 | - Added TypeScript type definitions. See [#1](https://github.com/helmetjs/frameguard/pull/16) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 22 | - Created a changelog 23 | 24 | ### Changed 25 | 26 | - Updated some package metadata 27 | - Update some documentation 28 | - Excluded some files from npm package 29 | 30 | Changes in versions 3.0.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 31 | -------------------------------------------------------------------------------- /middlewares/x-frame-options/README.md: -------------------------------------------------------------------------------- 1 | # X-Frame-Options middleware 2 | 3 | The `X-Frame-Options` HTTP header restricts who can put your site in a frame which can help mitigate things like [clickjacking attacks](https://en.wikipedia.org/wiki/Clickjacking). The header has two modes: `DENY` and `SAMEORIGIN`. 4 | 5 | This header is superseded by [the `frame-ancestors` Content Security Policy directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) but is still useful on old browsers. 6 | 7 | If your app does not need to be framed (and most don't) you can use `DENY`. If your site can be in frames from the same origin, you can set it to `SAMEORIGIN`. 8 | 9 | Usage: 10 | 11 | ```javascript 12 | const frameguard = require("frameguard"); 13 | 14 | // Don't allow me to be in ANY frames: 15 | app.use(frameguard({ action: "deny" })); 16 | 17 | // Only let me be framed by people of the same origin: 18 | app.use(frameguard({ action: "sameorigin" })); 19 | app.use(frameguard()); // defaults to sameorigin 20 | ``` 21 | 22 | A legacy action, `ALLOW-FROM`, is not supported by this middleware. [Read more here.](https://github.com/helmetjs/helmet/wiki/How-to-use-X%E2%80%93Frame%E2%80%93Options's-%60ALLOW%E2%80%93FROM%60-directive) 23 | -------------------------------------------------------------------------------- /middlewares/x-frame-options/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface XFrameOptionsOptions { 4 | action?: "deny" | "sameorigin"; 5 | } 6 | 7 | function getHeaderValueFromOptions({ 8 | action = "sameorigin", 9 | }: Readonly): string { 10 | const normalizedAction = 11 | typeof action === "string" ? action.toUpperCase() : action; 12 | 13 | switch (normalizedAction) { 14 | case "SAME-ORIGIN": 15 | return "SAMEORIGIN"; 16 | case "DENY": 17 | case "SAMEORIGIN": 18 | return normalizedAction; 19 | default: 20 | throw new Error( 21 | `X-Frame-Options received an invalid action ${JSON.stringify(action)}`, 22 | ); 23 | } 24 | } 25 | 26 | function xFrameOptions(options: Readonly = {}) { 27 | const headerValue = getHeaderValueFromOptions(options); 28 | 29 | return function xFrameOptionsMiddleware( 30 | _req: IncomingMessage, 31 | res: ServerResponse, 32 | next: () => void, 33 | ) { 34 | res.setHeader("X-Frame-Options", headerValue); 35 | next(); 36 | }; 37 | } 38 | 39 | export default xFrameOptions; 40 | -------------------------------------------------------------------------------- /middlewares/x-frame-options/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frameguard", 3 | "description": "Middleware to set X-Frame-Options headers", 4 | "version": "4.0.0", 5 | "keywords": ["express", "security", "x-frame-options", "clickjack"], 6 | "engines": { 7 | "node": ">=10.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /middlewares/x-permitted-cross-domain-policies/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - **Breaking:** increase TypeScript strictness around arguments. Only affects TypeScript users. See [helmetjs/helmet#369](https://github.com/helmetjs/helmet/issues/369) 8 | 9 | ### Removed 10 | 11 | - Dropped support for old Node versions. Node 14+ is now required 12 | 13 | ## 0.5.0 - 2019-09-01 14 | 15 | ## Changed 16 | 17 | - Dropped support for Node <8 18 | 19 | ## 0.4.0 - 2019-06-15 20 | 21 | ### Added 22 | 23 | - Added TypeScript type definitions. See [#7](https://github.com/helmetjs/crossdomain/issues/7) 24 | - Created a changelog 25 | - Added additional package metadata 26 | 27 | ### Changed 28 | 29 | - Excluded some files from npm package 30 | 31 | Changes in versions 0.3.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 32 | -------------------------------------------------------------------------------- /middlewares/x-permitted-cross-domain-policies/README.md: -------------------------------------------------------------------------------- 1 | # X-Permitted-Cross-Domain-Policies middleware 2 | 3 | The `X-Permitted-Cross-Domain-Policies` header tells some web clients (like Adobe Flash or Adobe Acrobat) your domain's policy for loading cross-domain content. See the description on [OWASP](https://owasp.org/www-project-secure-headers/) for more. 4 | 5 | Usage: 6 | 7 | ```javascript 8 | const crossdomain = require("helmet-crossdomain"); 9 | 10 | // Sets X-Permitted-Cross-Domain-Policies: none 11 | app.use(crossdomain()); 12 | 13 | // You can use any of the following values: 14 | app.use(crossdomain({ permittedPolicies: "none" })); 15 | app.use(crossdomain({ permittedPolicies: "master-only" })); 16 | app.use(crossdomain({ permittedPolicies: "by-content-type" })); 17 | app.use(crossdomain({ permittedPolicies: "all" })); 18 | ``` 19 | 20 | The `by-ftp-type` is not currently supported. Please open an issue or pull request if you desire this feature! 21 | 22 | If you don't expect Adobe products to load data from your site, you get a minor security benefit by adding this header. 23 | -------------------------------------------------------------------------------- /middlewares/x-permitted-cross-domain-policies/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | export interface XPermittedCrossDomainPoliciesOptions { 4 | permittedPolicies?: "none" | "master-only" | "by-content-type" | "all"; 5 | } 6 | 7 | const ALLOWED_PERMITTED_POLICIES = new Set([ 8 | "none", 9 | "master-only", 10 | "by-content-type", 11 | "all", 12 | ]); 13 | 14 | function getHeaderValueFromOptions({ 15 | permittedPolicies = "none", 16 | }: Readonly): string { 17 | if (ALLOWED_PERMITTED_POLICIES.has(permittedPolicies)) { 18 | return permittedPolicies; 19 | } else { 20 | throw new Error( 21 | `X-Permitted-Cross-Domain-Policies does not support ${JSON.stringify( 22 | permittedPolicies, 23 | )}`, 24 | ); 25 | } 26 | } 27 | 28 | function xPermittedCrossDomainPolicies( 29 | options: Readonly = {}, 30 | ) { 31 | const headerValue = getHeaderValueFromOptions(options); 32 | 33 | return function xPermittedCrossDomainPoliciesMiddleware( 34 | _req: IncomingMessage, 35 | res: ServerResponse, 36 | next: () => void, 37 | ) { 38 | res.setHeader("X-Permitted-Cross-Domain-Policies", headerValue); 39 | next(); 40 | }; 41 | } 42 | 43 | export default xPermittedCrossDomainPolicies; 44 | -------------------------------------------------------------------------------- /middlewares/x-permitted-cross-domain-policies/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helmet-crossdomain", 3 | "author": "Evan Hahn (https://evanhahn.com)", 4 | "contributors": [], 5 | "description": "Set the X-Permitted-Cross-Domain-Policies header in Express apps", 6 | "version": "0.5.0", 7 | "keywords": [ 8 | "express", 9 | "security", 10 | "crossdomain.xml", 11 | "x-permitted-cross-domain-policies" 12 | ], 13 | "engines": { 14 | "node": ">=10.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /middlewares/x-powered-by/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 - Unreleased 4 | 5 | ### Removed 6 | 7 | - Removed `setTo` option. See [this article](https://github.com/helmetjs/helmet/wiki/How-to-set-a-custom-X%E2%80%93Powered%E2%80%93By-header) to see how to replicate the removed behavior. See [#224](https://github.com/helmetjs/helmet/issues/224). 8 | - Dropped support for old Node versions. Node 10+ is now required 9 | 10 | ## 1.1.0 - 2019-05-26 11 | 12 | ### Added 13 | 14 | - Added TypeScript type definitions. See [#2](https://github.com/helmetjs/hide-powered-by/issues/2) and [helmetjs/helmet#188](https://github.com/helmetjs/helmet/issues/188) 15 | - Created a changelog 16 | 17 | ### Changed 18 | 19 | - Excluded some files from npm package 20 | 21 | Changes in versions 1.0.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 22 | -------------------------------------------------------------------------------- /middlewares/x-powered-by/README.md: -------------------------------------------------------------------------------- 1 | # X-Powered-By middleware 2 | 3 | Simple middleware to remove the `X-Powered-By` HTTP header if it's set. 4 | 5 | Hackers can exploit known vulnerabilities in Express/Node if they see that your site is powered by Express (or whichever framework you use). For example, `X-Powered-By: Express` is sent in every HTTP request coming from Express, by default. This won't provide much security benefit ([as discussed here](https://github.com/expressjs/express/pull/2813#issuecomment-159270428)), but might help a tiny bit. It will also improve performance by reducing the number of bytes sent. 6 | 7 | ```javascript 8 | const hidePoweredBy = require("hide-powered-by"); 9 | app.use(hidePoweredBy()); 10 | ``` 11 | 12 | Note: if you're using Express, you don't need this middleware and can just do this: 13 | 14 | ```javascript 15 | app.disable("x-powered-by"); 16 | ``` 17 | -------------------------------------------------------------------------------- /middlewares/x-powered-by/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | function xPoweredBy() { 4 | return function xPoweredByMiddleware( 5 | _req: IncomingMessage, 6 | res: ServerResponse, 7 | next: () => void, 8 | ) { 9 | res.removeHeader("X-Powered-By"); 10 | next(); 11 | }; 12 | } 13 | 14 | export default xPoweredBy; 15 | -------------------------------------------------------------------------------- /middlewares/x-powered-by/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hide-powered-by", 3 | "contributors": [ 4 | "Evan Hahn (https://evanhahn.com)", 5 | "Ameen Abdeen " 6 | ], 7 | "description": "Middleware to remove the X-Powered-By header", 8 | "version": "1.1.0", 9 | "keywords": ["express", "security", "x-powered-by", "powered-by"], 10 | "engines": { 11 | "node": ">=10.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middlewares/x-xss-protection/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 - 2020-08-02 4 | 5 | ### Changed 6 | 7 | - XSS filtering is now disabled by default. See [#230](https://github.com/helmetjs/helmet/issues/230) 8 | 9 | ### Removed 10 | 11 | - No longer accepts options. Read ["How to disable blocking with X–XSS–Protection"](https://github.com/helmetjs/helmet/wiki/How-to-disable-blocking-with-X%E2%80%93XSS%E2%80%93Protection) and ["How to enable the `report` directive with X–XSS–Protection"](https://github.com/helmetjs/helmet/wiki/How-to-enable-the-%60report%60-directive-with-X%E2%80%93XSS%E2%80%93Protection) if you need the legacy behavior. 12 | - Dropped support for old Node versions. Node 10+ is now required 13 | 14 | ## 1.3.0 - 2019-09-01 15 | 16 | ### Added 17 | 18 | - Added `mode: null` to disable `mode=block` 19 | 20 | ### Changed 21 | 22 | - Minor performance improvements with Internet Explorer <9 detection 23 | 24 | ## 1.2.0 - 2019-06-15 25 | 26 | ### Added 27 | 28 | - Added TypeScript type definitions. See [#8](https://github.com/helmetjs/x-xss-protection/pull/8) 29 | - Created a changelog 30 | - Added some additional package metadata 31 | 32 | ### Changed 33 | 34 | - Updated documentation 35 | - Excluded some files from npm package 36 | 37 | Changes in versions 1.1.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md). 38 | -------------------------------------------------------------------------------- /middlewares/x-xss-protection/README.md: -------------------------------------------------------------------------------- 1 | # X-XSS-Protection middleware 2 | 3 | The `X-XSS-Protection` HTTP header aimed to offer a basic protection against cross-site scripting (XSS) attacks. _However, you probably should disable it_, which is what this middleware does. 4 | 5 | Many browsers have chosen to remove it because of the unintended security issues it creates. Generally, you should protect against XSS with sanitization and a Content Security Policy. For more, read [this GitHub issue](https://github.com/helmetjs/helmet/issues/230). 6 | 7 | This middleware sets the `X-XSS-Protection` header to `0`. For example: 8 | 9 | ```javascript 10 | const xXssProtection = require("x-xss-protection"); 11 | 12 | // Set "X-XSS-Protection: 0" 13 | app.use(xXssProtection()); 14 | ``` 15 | 16 | If you truly need the legacy behavior, you can write your own simple middleware and avoid installing this module. For example: 17 | 18 | ```javascript 19 | // NOTE: This is probably insecure! 20 | app.use((req, res, next) => { 21 | res.setHeader("X-XSS-Protection", "1; mode=block"); 22 | next(); 23 | }); 24 | ``` 25 | -------------------------------------------------------------------------------- /middlewares/x-xss-protection/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | 3 | function xXssProtection() { 4 | return function xXssProtectionMiddleware( 5 | _req: IncomingMessage, 6 | res: ServerResponse, 7 | next: () => void, 8 | ) { 9 | res.setHeader("X-XSS-Protection", "0"); 10 | next(); 11 | }; 12 | } 13 | 14 | export default xXssProtection; 15 | -------------------------------------------------------------------------------- /middlewares/x-xss-protection/package-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x-xss-protection", 3 | "description": "Middleware to disable the X-XSS-Protection header", 4 | "version": "2.0.0", 5 | "keywords": ["express", "security", "x-xss-protection"], 6 | "engines": { 7 | "node": ">=10.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "8.1.0", 4 | "devDependencies": { 5 | "@eslint/js": "^9.25.1", 6 | "@rollup/plugin-typescript": "^12.1.2", 7 | "@types/connect": "^3.4.38", 8 | "@types/node": "^22.15.3", 9 | "@types/node-zopfli": "^2.0.5", 10 | "@types/supertest": "^6.0.3", 11 | "connect": "^3.7.0", 12 | "eslint": "^9.25.1", 13 | "globals": "^16.0.0", 14 | "node-zopfli": "^2.1.4", 15 | "npm-run-all2": "^7.0.2", 16 | "prettier": "^3.5.3", 17 | "rollup": "^4.40.1", 18 | "rollup-plugin-dts": "^6.2.1", 19 | "supertest": "^7.1.0", 20 | "tsx": "^4.19.4", 21 | "typescript": "^5.8.3", 22 | "typescript-eslint": "^8.31.1" 23 | }, 24 | "scripts": { 25 | "test:eslint": "eslint --cache .", 26 | "test:node": "tsx --test test/*.ts", 27 | "test:prettier": "prettier --check .", 28 | "test:typescript": "tsc --noEmit", 29 | "format": "prettier --write .", 30 | "clean": "node ./bin/clean.mjs", 31 | "build": "tsx ./build/build-package.ts", 32 | "test": "run-p --aggregate-output test:*" 33 | }, 34 | "type": "module", 35 | "engines": { 36 | "node": ">=18.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/content-security-policy.test.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import assert from "node:assert/strict"; 3 | import { IncomingMessage, ServerResponse } from "node:http"; 4 | import { describe, it } from "node:test"; 5 | import supertest from "supertest"; 6 | import contentSecurityPolicy, { 7 | dangerouslyDisableDefaultSrc, 8 | getDefaultDirectives, 9 | } from "../middlewares/content-security-policy"; 10 | import { check } from "./helpers"; 11 | 12 | const shouldBeQuoted = [ 13 | "none", 14 | "self", 15 | "strict-dynamic", 16 | "report-sample", 17 | "inline-speculation-rules", 18 | "unsafe-inline", 19 | "unsafe-eval", 20 | "unsafe-hashes", 21 | "wasm-unsafe-eval", 22 | "nonce-abc123", 23 | "sha256-ks9D5epDKP+c2x6DrkuHmhmfKkOM/HZ+pOlzdWbI91k=", 24 | ]; 25 | 26 | const getOwn = ( 27 | obj: T, 28 | key: K, 29 | ): T[K] | undefined => (Object.hasOwn(obj, key) ? obj[key] : undefined); 30 | 31 | async function checkCsp({ 32 | middlewareArgs, 33 | expectedHeader = "content-security-policy", 34 | expectedDirectives, 35 | }: Readonly<{ 36 | middlewareArgs: Parameters; 37 | expectedHeader?: string; 38 | expectedDirectives: Set; 39 | }>): Promise { 40 | const { header } = await check(contentSecurityPolicy(...middlewareArgs), {}); 41 | const headerValue = getOwn(header, expectedHeader); 42 | assert( 43 | typeof headerValue === "string", 44 | `${expectedHeader} header should be set`, 45 | ); 46 | const actualDirectives = new Set(headerValue.split(";")); 47 | assert.deepEqual(actualDirectives, expectedDirectives); 48 | } 49 | 50 | describe("Content-Security-Policy middleware", () => { 51 | it("sets a default policy when passed no directives", async () => { 52 | const expectedDirectives = new Set([ 53 | "default-src 'self'", 54 | "base-uri 'self'", 55 | "font-src 'self' https: data:", 56 | "form-action 'self'", 57 | "frame-ancestors 'self'", 58 | "img-src 'self' data:", 59 | "object-src 'none'", 60 | "script-src 'self'", 61 | "script-src-attr 'none'", 62 | "style-src 'self' https: 'unsafe-inline'", 63 | "upgrade-insecure-requests", 64 | ]); 65 | await checkCsp({ 66 | middlewareArgs: [], 67 | expectedDirectives, 68 | }); 69 | await checkCsp({ 70 | middlewareArgs: [{}], 71 | expectedDirectives, 72 | }); 73 | await checkCsp({ 74 | middlewareArgs: [Object.create(null)], 75 | expectedDirectives, 76 | }); 77 | await checkCsp({ 78 | middlewareArgs: [{ directives: undefined }], 79 | expectedDirectives, 80 | }); 81 | await checkCsp({ 82 | middlewareArgs: [{ useDefaults: true }], 83 | expectedDirectives, 84 | }); 85 | }); 86 | 87 | it("sets directives when named with snake-case", async () => { 88 | await checkCsp({ 89 | middlewareArgs: [ 90 | { 91 | useDefaults: false, 92 | directives: { 93 | "default-src": ["'self'"], 94 | "script-src": ["example.com"], 95 | "style-src": ["'none'"], 96 | }, 97 | }, 98 | ], 99 | expectedDirectives: new Set([ 100 | "default-src 'self'", 101 | "script-src example.com", 102 | "style-src 'none'", 103 | ]), 104 | }); 105 | }); 106 | 107 | it("sets directives when named with camelCase", async () => { 108 | await checkCsp({ 109 | middlewareArgs: [ 110 | { 111 | useDefaults: false, 112 | directives: { 113 | defaultSrc: ["'self'"], 114 | scriptSrc: ["example.com"], 115 | styleSrc: ["'none'"], 116 | }, 117 | }, 118 | ], 119 | expectedDirectives: new Set([ 120 | "default-src 'self'", 121 | "script-src example.com", 122 | "style-src 'none'", 123 | ]), 124 | }); 125 | }); 126 | 127 | it("accepts a mix of snake-case and camelCase directive names", async () => { 128 | await checkCsp({ 129 | middlewareArgs: [ 130 | { 131 | useDefaults: false, 132 | directives: { 133 | "default-src": ["'self'"], 134 | "script-src": ["example.com"], 135 | styleSrc: ["'none'"], 136 | objectSrc: ["'none'"], 137 | }, 138 | }, 139 | ], 140 | expectedDirectives: new Set([ 141 | "default-src 'self'", 142 | "script-src example.com", 143 | "style-src 'none'", 144 | "object-src 'none'", 145 | ]), 146 | }); 147 | }); 148 | 149 | it("accepts an empty list of directive values", async () => { 150 | await checkCsp({ 151 | middlewareArgs: [ 152 | { 153 | useDefaults: false, 154 | directives: { 155 | "default-src": ["'self'"], 156 | sandbox: [], 157 | }, 158 | }, 159 | ], 160 | expectedDirectives: new Set(["default-src 'self'", "sandbox"]), 161 | }); 162 | }); 163 | 164 | it("accepts non-array iterables for directive values", async () => { 165 | await checkCsp({ 166 | middlewareArgs: [ 167 | { 168 | useDefaults: false, 169 | directives: { 170 | "default-src": new Set(["'self'"]), 171 | sandbox: { 172 | [Symbol.iterator]: () => ({ 173 | next: () => ({ 174 | done: true, 175 | value: undefined, 176 | }), 177 | }), 178 | }, 179 | }, 180 | }, 181 | ], 182 | expectedDirectives: new Set(["default-src 'self'", "sandbox"]), 183 | }); 184 | }); 185 | 186 | it("accepts strings as directive values", async () => { 187 | await checkCsp({ 188 | middlewareArgs: [ 189 | { 190 | useDefaults: false, 191 | directives: { 192 | "default-src": "'self' example.com", 193 | scriptSrc: "'none'", 194 | sandbox: "", 195 | }, 196 | }, 197 | ], 198 | expectedDirectives: new Set([ 199 | "default-src 'self' example.com", 200 | "script-src 'none'", 201 | "sandbox", 202 | ]), 203 | }); 204 | }); 205 | 206 | it("treats null directive values as nothing, as if they weren't set", async () => { 207 | await checkCsp({ 208 | middlewareArgs: [ 209 | { 210 | useDefaults: false, 211 | directives: { 212 | "default-src": "'self'", 213 | scriptSrc: null, 214 | }, 215 | }, 216 | ], 217 | expectedDirectives: new Set(["default-src 'self'"]), 218 | }); 219 | }); 220 | 221 | it("allows functions in directive values to generate dynamic directives", async () => { 222 | await checkCsp({ 223 | middlewareArgs: [ 224 | { 225 | useDefaults: false, 226 | directives: { 227 | "default-src": [ 228 | "'self'", 229 | (req: IncomingMessage, res: ServerResponse) => { 230 | assert( 231 | req instanceof IncomingMessage, 232 | "req should be a request", 233 | ); 234 | assert( 235 | res instanceof ServerResponse, 236 | "res should be a response", 237 | ); 238 | return "foo.example.com"; 239 | }, 240 | "bar.example.com", 241 | ], 242 | }, 243 | }, 244 | ], 245 | expectedDirectives: new Set([ 246 | "default-src 'self' foo.example.com bar.example.com", 247 | ]), 248 | }); 249 | }); 250 | 251 | it("can override the default options", async () => { 252 | const expectedDirectives = new Set([ 253 | "default-src 'self' example.com", 254 | "font-src 'self' https: data:", 255 | "form-action 'self'", 256 | "frame-ancestors 'self'", 257 | "img-src 'self' data:", 258 | "object-src 'none'", 259 | "script-src example.com", 260 | "script-src-attr 'none'", 261 | "style-src 'self' https: 'unsafe-inline'", 262 | "upgrade-insecure-requests", 263 | ]); 264 | 265 | await checkCsp({ 266 | middlewareArgs: [ 267 | { 268 | useDefaults: true, 269 | directives: { 270 | "default-src": ["'self'", "example.com"], 271 | "base-uri": null, 272 | scriptSrc: ["example.com"], 273 | }, 274 | }, 275 | ], 276 | expectedDirectives, 277 | }); 278 | 279 | await checkCsp({ 280 | middlewareArgs: [ 281 | { 282 | directives: { 283 | "default-src": ["'self'", "example.com"], 284 | "base-uri": null, 285 | scriptSrc: ["example.com"], 286 | }, 287 | }, 288 | ], 289 | expectedDirectives, 290 | }); 291 | }); 292 | 293 | it('can set the "report only" version of the header instead', async () => { 294 | await checkCsp({ 295 | middlewareArgs: [ 296 | { 297 | useDefaults: false, 298 | directives: { 299 | "default-src": "'self'", 300 | }, 301 | reportOnly: true, 302 | }, 303 | ], 304 | expectedHeader: "content-security-policy-report-only", 305 | expectedDirectives: new Set(["default-src 'self'"]), 306 | }); 307 | }); 308 | 309 | it("throws if any directive names are invalid", () => { 310 | const invalidNames = [ 311 | "", 312 | ";", 313 | "\u00e1", 314 | "default src", 315 | "default;src", 316 | "default,src", 317 | "default!src", 318 | "def\u00e1ult-src", 319 | "default_src", 320 | "__proto__", 321 | ]; 322 | for (const name of invalidNames) { 323 | assert.throws( 324 | () => { 325 | contentSecurityPolicy({ 326 | useDefaults: true, 327 | directives: { 328 | [name]: ["value"], 329 | }, 330 | }); 331 | }, 332 | { 333 | message: 334 | /^Content-Security-Policy received an invalid directive name "/, 335 | }, 336 | ); 337 | } 338 | }); 339 | 340 | it("throws if duplicate directive names are found", () => { 341 | assert.throws( 342 | () => { 343 | contentSecurityPolicy({ 344 | useDefaults: false, 345 | directives: { 346 | defaultSrc: ["foo"], 347 | "default-src": ["foo"], 348 | }, 349 | }); 350 | }, 351 | { 352 | message: 353 | /^Content-Security-Policy received a duplicate directive "default-src"$/, 354 | }, 355 | ); 356 | 357 | assert.throws( 358 | () => { 359 | contentSecurityPolicy({ 360 | useDefaults: false, 361 | directives: { 362 | defaultSrc: ["'self'"], 363 | scriptSrc: ["foo"], 364 | "script-src": ["foo"], 365 | }, 366 | }); 367 | }, 368 | { 369 | message: 370 | /^Content-Security-Policy received a duplicate directive "script-src"$/, 371 | }, 372 | ); 373 | }); 374 | 375 | it("throws if any directive values are invalid", () => { 376 | const invalidValues = [";", ",", "hello;world", "hello,world"]; 377 | for (const invalidValue of invalidValues) { 378 | assert.throws( 379 | () => { 380 | contentSecurityPolicy({ 381 | useDefaults: false, 382 | directives: { 383 | "default-src": "'self'", 384 | "something-else": [invalidValue], 385 | }, 386 | }); 387 | }, 388 | { 389 | message: 390 | /^Content-Security-Policy received an invalid directive value for "something-else"$/, 391 | }, 392 | ); 393 | } 394 | 395 | for (const invalidDirectiveEntry of shouldBeQuoted) { 396 | assert.throws( 397 | () => { 398 | contentSecurityPolicy({ 399 | useDefaults: false, 400 | directives: { 401 | "default-src": "'self'", 402 | "something-else": [invalidDirectiveEntry], 403 | }, 404 | }); 405 | }, 406 | { 407 | message: `Content-Security-Policy received an invalid directive value for "something-else". "${invalidDirectiveEntry}" should be quoted`, 408 | }, 409 | ); 410 | } 411 | }); 412 | 413 | it("errors if any directive values are invalid when a function returns", async () => { 414 | const badDirectiveValueEntries = ["bad;value", ...shouldBeQuoted]; 415 | 416 | await Promise.all( 417 | badDirectiveValueEntries.map(async (directiveValueEntry) => { 418 | const app = connect() 419 | .use( 420 | contentSecurityPolicy({ 421 | useDefaults: false, 422 | directives: { 423 | defaultSrc: ["'self'", () => directiveValueEntry], 424 | }, 425 | }), 426 | ) 427 | .use( 428 | ( 429 | err: Error, 430 | _req: IncomingMessage, 431 | res: ServerResponse, 432 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 433 | _next: () => void, 434 | ) => { 435 | const isOk = err.message.startsWith( 436 | 'Content-Security-Policy received an invalid directive value for "default-src"', 437 | ); 438 | res.end(JSON.stringify(isOk)); 439 | }, 440 | ); 441 | 442 | await supertest(app).get("/").expect(200, "true"); 443 | }), 444 | ); 445 | }); 446 | 447 | it("throws if default-src is missing", () => { 448 | assert.throws( 449 | () => { 450 | contentSecurityPolicy({ 451 | useDefaults: false, 452 | directives: {}, 453 | }); 454 | }, 455 | { 456 | message: 457 | /^Content-Security-Policy has no directives. Either set some or disable the header$/, 458 | }, 459 | ); 460 | assert.throws( 461 | () => { 462 | contentSecurityPolicy({ 463 | useDefaults: false, 464 | directives: { 465 | scriptSrc: ["example.com"], 466 | }, 467 | }); 468 | }, 469 | { 470 | message: 471 | /^Content-Security-Policy needs a default-src but none was provided. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.$/, 472 | }, 473 | ); 474 | assert.throws( 475 | () => { 476 | contentSecurityPolicy({ 477 | directives: { defaultSrc: null }, 478 | }); 479 | }, 480 | { 481 | message: 482 | /^Content-Security-Policy needs a default-src but it was set to `null`. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.$/, 483 | }, 484 | ); 485 | 486 | // These should not throw. 487 | contentSecurityPolicy({ 488 | useDefaults: false, 489 | directives: { 490 | defaultSrc: ["foo"], 491 | }, 492 | }); 493 | contentSecurityPolicy({ 494 | useDefaults: false, 495 | directives: { 496 | "default-src": ["foo"], 497 | }, 498 | }); 499 | contentSecurityPolicy({ 500 | useDefaults: false, 501 | directives: { 502 | defaultSrc: [], 503 | }, 504 | }); 505 | contentSecurityPolicy({ 506 | useDefaults: false, 507 | directives: { 508 | defaultSrc: "", 509 | }, 510 | }); 511 | }); 512 | 513 | it("allows default-src to be explicitly disabled", async () => { 514 | await checkCsp({ 515 | middlewareArgs: [ 516 | { 517 | useDefaults: false, 518 | directives: { 519 | defaultSrc: dangerouslyDisableDefaultSrc, 520 | scriptSrc: ["example.com"], 521 | }, 522 | }, 523 | ], 524 | expectedDirectives: new Set(["script-src example.com"]), 525 | }); 526 | 527 | await checkCsp({ 528 | middlewareArgs: [ 529 | { 530 | useDefaults: false, 531 | directives: { 532 | "default-src": dangerouslyDisableDefaultSrc, 533 | "script-src": ["example.com"], 534 | }, 535 | }, 536 | ], 537 | expectedDirectives: new Set(["script-src example.com"]), 538 | }); 539 | 540 | await checkCsp({ 541 | middlewareArgs: [ 542 | { 543 | useDefaults: true, 544 | directives: { 545 | "default-src": dangerouslyDisableDefaultSrc, 546 | }, 547 | }, 548 | ], 549 | expectedDirectives: new Set([ 550 | "base-uri 'self'", 551 | "font-src 'self' https: data:", 552 | "form-action 'self'", 553 | "frame-ancestors 'self'", 554 | "img-src 'self' data:", 555 | "object-src 'none'", 556 | "script-src 'self'", 557 | "script-src-attr 'none'", 558 | "style-src 'self' https: 'unsafe-inline'", 559 | "upgrade-insecure-requests", 560 | ]), 561 | }); 562 | }); 563 | 564 | it("throws an error if default-src is disabled and there are no other directives", () => { 565 | assert.throws( 566 | () => { 567 | contentSecurityPolicy({ 568 | useDefaults: false, 569 | directives: { 570 | defaultSrc: dangerouslyDisableDefaultSrc, 571 | }, 572 | }); 573 | }, 574 | { 575 | message: 576 | /^Content-Security-Policy has no directives. Either set some or disable the header$/, 577 | }, 578 | ); 579 | 580 | assert.throws( 581 | () => { 582 | contentSecurityPolicy({ 583 | useDefaults: false, 584 | directives: { 585 | "default-src": dangerouslyDisableDefaultSrc, 586 | }, 587 | }); 588 | }, 589 | { 590 | message: 591 | /^Content-Security-Policy has no directives. Either set some or disable the header$/, 592 | }, 593 | ); 594 | }); 595 | 596 | it("throws an error if directives other than default-src are `dangerouslyDisableDefaultSrc`", () => { 597 | assert.throws( 598 | () => { 599 | contentSecurityPolicy({ 600 | directives: { 601 | "default-src": "'self'", 602 | "script-src": dangerouslyDisableDefaultSrc, 603 | }, 604 | }); 605 | }, 606 | { 607 | message: 608 | /^Content-Security-Policy: tried to disable "script-src" as if it were default-src; simply omit the key$/, 609 | }, 610 | ); 611 | }); 612 | }); 613 | 614 | describe("getDefaultDirectives", () => { 615 | it("returns the middleware's default directives", () => { 616 | assert.deepEqual(getDefaultDirectives(), { 617 | "base-uri": ["'self'"], 618 | "default-src": ["'self'"], 619 | "font-src": ["'self'", "https:", "data:"], 620 | "form-action": ["'self'"], 621 | "frame-ancestors": ["'self'"], 622 | "img-src": ["'self'", "data:"], 623 | "object-src": ["'none'"], 624 | "script-src": ["'self'"], 625 | "script-src-attr": ["'none'"], 626 | "style-src": ["'self'", "https:", "'unsafe-inline'"], 627 | "upgrade-insecure-requests": [], 628 | }); 629 | }); 630 | 631 | it("attaches itself to the top-level function", () => { 632 | assert.equal( 633 | getDefaultDirectives, 634 | contentSecurityPolicy.getDefaultDirectives, 635 | ); 636 | }); 637 | 638 | it("returns a new copy each time", () => { 639 | const one = getDefaultDirectives(); 640 | one["worker-src"] = ["ignored.example"]; 641 | (one["img-src"] as Array).push("ignored.example"); 642 | 643 | const two = getDefaultDirectives(); 644 | assert(!("worker-src" in two)); 645 | assert( 646 | two["img-src"] && !Array.from(two["img-src"]).includes("ignored.example"), 647 | ); 648 | }); 649 | }); 650 | -------------------------------------------------------------------------------- /test/cross-origin-embedder-policy.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import crossOriginEmbedderPolicy from "../middlewares/cross-origin-embedder-policy"; 4 | import { check } from "./helpers"; 5 | 6 | describe("Cross-Origin-Embedder-Policy middleware", () => { 7 | it('sets "Cross-Origin-Embedder-Policy: same-origin" when called with no policy', async () => { 8 | const expectedHeaders = { 9 | "cross-origin-embedder-policy": "require-corp", 10 | }; 11 | await check(crossOriginEmbedderPolicy(), expectedHeaders); 12 | await check(crossOriginEmbedderPolicy({}), expectedHeaders); 13 | await check( 14 | crossOriginEmbedderPolicy(Object.create(null)), 15 | expectedHeaders, 16 | ); 17 | await check( 18 | crossOriginEmbedderPolicy({ policy: undefined }), 19 | expectedHeaders, 20 | ); 21 | }); 22 | 23 | (["require-corp", "credentialless", "unsafe-none"] as const).forEach( 24 | (policy) => { 25 | it(`sets "Cross-Origin-Embedder-Policy: ${policy}" when told to`, async () => { 26 | await check(crossOriginEmbedderPolicy({ policy }), { 27 | "cross-origin-embedder-policy": policy, 28 | }); 29 | }); 30 | }, 31 | ); 32 | 33 | it("throws when setting the policy to an invalid value", () => { 34 | const invalidValues = [ 35 | "", 36 | "foo", 37 | "CREDENTIALLESS", 38 | 123, 39 | null, 40 | new String("credentialless"), 41 | ]; 42 | for (const policy of invalidValues) { 43 | assert.throws( 44 | () => crossOriginEmbedderPolicy({ policy: policy as any }), 45 | { 46 | message: /^Cross-Origin-Embedder-Policy does not support /, 47 | }, 48 | ); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/cross-origin-opener-policy.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import crossOriginOpenerPolicy from "../middlewares/cross-origin-opener-policy"; 4 | import { check } from "./helpers"; 5 | 6 | describe("Cross-Origin-Opener-Policy middleware", () => { 7 | it('sets "Cross-Origin-Opener-Policy: same-origin" when called with no policy', async () => { 8 | const expectedHeaders = { 9 | "cross-origin-opener-policy": "same-origin", 10 | }; 11 | await check(crossOriginOpenerPolicy(), expectedHeaders); 12 | await check(crossOriginOpenerPolicy({}), expectedHeaders); 13 | await check(crossOriginOpenerPolicy(Object.create(null)), expectedHeaders); 14 | await check( 15 | crossOriginOpenerPolicy({ policy: undefined }), 16 | expectedHeaders, 17 | ); 18 | }); 19 | 20 | (["same-origin", "same-origin-allow-popups", "unsafe-none"] as const).forEach( 21 | (policy) => { 22 | it(`sets "Cross-Origin-Opener-Policy: ${policy}" when told to`, async () => { 23 | await check(crossOriginOpenerPolicy({ policy }), { 24 | "cross-origin-opener-policy": policy, 25 | }); 26 | }); 27 | }, 28 | ); 29 | 30 | it("throws when setting the policy to an invalid value", () => { 31 | const invalidValues = [ 32 | "", 33 | "foo", 34 | "SAME-ORIGIN", 35 | 123, 36 | null, 37 | new String("same-origin"), 38 | ]; 39 | for (const policy of invalidValues) { 40 | assert.throws(() => crossOriginOpenerPolicy({ policy: policy as any }), { 41 | message: /^Cross-Origin-Opener-Policy does not support /, 42 | }); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/cross-origin-resource-policy.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import crossOriginResourcePolicy from "../middlewares/cross-origin-resource-policy"; 4 | import { check } from "./helpers"; 5 | 6 | describe("Cross-Origin-Resource-Policy middleware", () => { 7 | it('sets "Cross-Origin-Resource-Policy: same-origin" when called with no policy', async () => { 8 | const expectedHeaders = { 9 | "cross-origin-resource-policy": "same-origin", 10 | }; 11 | await check(crossOriginResourcePolicy(), expectedHeaders); 12 | await check(crossOriginResourcePolicy({}), expectedHeaders); 13 | await check( 14 | crossOriginResourcePolicy(Object.create(null)), 15 | expectedHeaders, 16 | ); 17 | await check( 18 | crossOriginResourcePolicy({ policy: undefined }), 19 | expectedHeaders, 20 | ); 21 | }); 22 | 23 | (["same-origin", "same-site", "cross-origin"] as const).forEach((policy) => { 24 | it(`sets "Cross-Origin-Resource-Policy: ${policy}" when told to`, async () => { 25 | await check(crossOriginResourcePolicy({ policy }), { 26 | "cross-origin-resource-policy": policy, 27 | }); 28 | }); 29 | }); 30 | 31 | it("throws when setting the policy to an invalid value", () => { 32 | const invalidValues = [ 33 | "", 34 | "foo", 35 | "CROSS-ORIGIN", 36 | 123, 37 | null, 38 | new String("none"), 39 | ]; 40 | for (const policy of invalidValues) { 41 | assert.throws( 42 | () => crossOriginResourcePolicy({ policy: policy as any }), 43 | { message: /^Cross-Origin-Resource-Policy does not support / }, 44 | ); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import assert from "node:assert/strict"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | type MiddlewareFunction = ( 7 | req: IncomingMessage, 8 | res: ServerResponse, 9 | next: () => void, 10 | ) => void; 11 | 12 | export async function check( 13 | middleware: MiddlewareFunction, 14 | expectedHeaders: Readonly>, 15 | ) { 16 | const app = connect() 17 | .use((_req, res, next) => { 18 | res.setHeader("X-Powered-By", "Helmet test"); 19 | next(); 20 | }) 21 | .use(middleware) 22 | .use((_req: IncomingMessage, res: ServerResponse) => { 23 | res.end("Hello world!"); 24 | }); 25 | 26 | const response = await supertest(app).get("/").expect(200, "Hello world!"); 27 | 28 | for (const [headerName, headerValue] of Object.entries(expectedHeaders)) { 29 | if (headerValue === null) { 30 | assert( 31 | !(headerName in response.header), 32 | `${headerName} should not be set`, 33 | ); 34 | } else { 35 | assert.equal( 36 | response.header[headerName], 37 | headerValue, 38 | `${headerName} should have value ${headerValue}`, 39 | ); 40 | } 41 | } 42 | 43 | return response; 44 | } 45 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import assert from "node:assert/strict"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import { describe, it, type TestContext, type Mock } from "node:test"; 5 | import supertest from "supertest"; 6 | import { check } from "./helpers"; 7 | 8 | import * as helmet from ".."; 9 | 10 | import contentSecurityPolicy from "../middlewares/content-security-policy"; 11 | import crossOriginEmbedderPolicy from "../middlewares/cross-origin-embedder-policy"; 12 | import crossOriginOpenerPolicy from "../middlewares/cross-origin-opener-policy"; 13 | import crossOriginResourcePolicy from "../middlewares/cross-origin-resource-policy"; 14 | import originAgentCluster from "../middlewares/origin-agent-cluster"; 15 | import referrerPolicy from "../middlewares/referrer-policy"; 16 | import strictTransportSecurity from "../middlewares/strict-transport-security"; 17 | import xContentTypeOptions from "../middlewares/x-content-type-options"; 18 | import xDnsPrefetchControl from "../middlewares/x-dns-prefetch-control"; 19 | import xDownloadOptions from "../middlewares/x-download-options"; 20 | import xFrameOptions from "../middlewares/x-frame-options"; 21 | import xPermittedCrossDomainPolicies from "../middlewares/x-permitted-cross-domain-policies"; 22 | import xPoweredBy from "../middlewares/x-powered-by"; 23 | import xXssProtection from "../middlewares/x-xss-protection"; 24 | 25 | describe("helmet", () => { 26 | const topLevel = helmet.default; 27 | 28 | it("includes all middleware, except COEP, with their default options", async () => { 29 | // NOTE: This test relies on the CSP object being ordered a certain way, 30 | // which could change (and be non-breaking). If that becomes a problem, 31 | // we should update this test to be more robust. 32 | const expectedHeaders = { 33 | "content-security-policy": 34 | "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", 35 | "cross-origin-embedder-policy": null, 36 | "cross-origin-opener-policy": "same-origin", 37 | "cross-origin-resource-policy": "same-origin", 38 | "origin-agent-cluster": "?1", 39 | "referrer-policy": "no-referrer", 40 | "strict-transport-security": "max-age=31536000; includeSubDomains", 41 | "x-content-type-options": "nosniff", 42 | "x-dns-prefetch-control": "off", 43 | "x-download-options": "noopen", 44 | "x-frame-options": "SAMEORIGIN", 45 | "x-permitted-cross-domain-policies": "none", 46 | "x-powered-by": null, 47 | "x-xss-protection": "0", 48 | }; 49 | 50 | await check(topLevel(), expectedHeaders); 51 | await check(topLevel({}), expectedHeaders); 52 | await check(topLevel(Object.create(null)), expectedHeaders); 53 | }); 54 | 55 | it("allows individual middlewares to be disabled", async () => { 56 | await check(topLevel({ contentSecurityPolicy: false }), { 57 | "content-security-policy": null, 58 | }); 59 | await check(topLevel({ xDnsPrefetchControl: false }), { 60 | "x-dns-prefetch-control": null, 61 | }); 62 | }); 63 | 64 | it("works with all default middlewares disabled", async () => { 65 | await check( 66 | topLevel({ 67 | contentSecurityPolicy: false, 68 | crossOriginEmbedderPolicy: false, 69 | originAgentCluster: false, 70 | referrerPolicy: false, 71 | strictTransportSecurity: false, 72 | xContentTypeOptions: false, 73 | xDnsPrefetchControl: false, 74 | xDownloadOptions: false, 75 | xFrameOptions: false, 76 | xPermittedCrossDomainPolicies: false, 77 | xPoweredBy: false, 78 | xXssProtection: false, 79 | }), 80 | { 81 | "content-security-policy": null, 82 | "x-frame-options": null, 83 | }, 84 | ); 85 | }); 86 | 87 | it("errors when `use`d directly", () => { 88 | const fakeRequest = { 89 | constructor: { 90 | name: "IncomingMessage", 91 | }, 92 | }; 93 | 94 | assert.throws(() => topLevel(fakeRequest as any)); 95 | }); 96 | 97 | it("allows default middleware to be explicitly enabled (a no-op)", async () => { 98 | await check(topLevel({ xFrameOptions: true }), { 99 | "x-frame-options": "SAMEORIGIN", 100 | }); 101 | }); 102 | 103 | it("allows Cross-Origin-Embedder-Policy middleware to be explicitly enabled", async () => { 104 | await check(topLevel({ crossOriginEmbedderPolicy: true }), { 105 | "cross-origin-embedder-policy": "require-corp", 106 | }); 107 | }); 108 | 109 | it("allows Cross-Origin-Embedder-Policy middleware to be explicitly disabled", async () => { 110 | await check(topLevel({ crossOriginEmbedderPolicy: false }), { 111 | "cross-origin-embedder-policy": null, 112 | }); 113 | }); 114 | 115 | it("allows Cross-Origin-Embedder-Policy middleware to be enabled with custom arguments", async () => { 116 | await check( 117 | topLevel({ crossOriginEmbedderPolicy: { policy: "credentialless" } }), 118 | { 119 | "cross-origin-embedder-policy": "credentialless", 120 | }, 121 | ); 122 | }); 123 | 124 | it("allows Cross-Origin-Opener-Policy middleware to be enabled with its default", async () => { 125 | await check(topLevel({ crossOriginOpenerPolicy: true }), { 126 | "cross-origin-opener-policy": "same-origin", 127 | }); 128 | }); 129 | 130 | it("allows Cross-Origin-Opener-Policy middleware to be enabled with custom arguments", async () => { 131 | await check( 132 | topLevel({ 133 | crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" }, 134 | }), 135 | { 136 | "cross-origin-opener-policy": "same-origin-allow-popups", 137 | }, 138 | ); 139 | }); 140 | 141 | it("allows Cross-Origin-Opener-Policy middleware to be explicitly disabled", async () => { 142 | await check(topLevel({ crossOriginOpenerPolicy: false }), { 143 | "cross-origin-opener-policy": null, 144 | }); 145 | }); 146 | 147 | it("allows Cross-Origin-Resource-Policy middleware to be enabled with its default", async () => { 148 | await check(topLevel({ crossOriginResourcePolicy: true }), { 149 | "cross-origin-resource-policy": "same-origin", 150 | }); 151 | }); 152 | 153 | it("allows Cross-Origin-Resource-Policy middleware to be enabled with custom arguments", async () => { 154 | await check( 155 | topLevel({ crossOriginResourcePolicy: { policy: "same-site" } }), 156 | { 157 | "cross-origin-resource-policy": "same-site", 158 | }, 159 | ); 160 | }); 161 | 162 | it("allows Cross-Origin-Resource-Policy middleware to be explicitly disabled", async () => { 163 | await check(topLevel({ crossOriginResourcePolicy: false }), { 164 | "cross-origin-resource-policy": null, 165 | }); 166 | }); 167 | 168 | it("allows Origin-Agent-Cluster middleware to be enabled", async () => { 169 | await check(topLevel({ originAgentCluster: true }), { 170 | "origin-agent-cluster": "?1", 171 | }); 172 | }); 173 | 174 | it("allows Origin-Agent-Cluster middleware to be explicitly disabled", async () => { 175 | await check(topLevel({ originAgentCluster: false }), { 176 | "origin-agent-cluster": null, 177 | }); 178 | }); 179 | 180 | it("properly handles a middleware calling `next()` with an error", async () => { 181 | const app = connect() 182 | .use( 183 | topLevel({ 184 | contentSecurityPolicy: { 185 | directives: { 186 | defaultSrc: ["'self'", () => "bad;value"], 187 | }, 188 | }, 189 | }), 190 | ) 191 | .use( 192 | ( 193 | err: Error, 194 | _req: IncomingMessage, 195 | res: ServerResponse, 196 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 197 | _next: () => void, 198 | ) => { 199 | res.statusCode = 500; 200 | res.setHeader("Content-Type", "application/json"); 201 | res.end(JSON.stringify({ message: err.message })); 202 | }, 203 | ); 204 | 205 | await supertest(app).get("/").expect(500, { 206 | message: 207 | 'Content-Security-Policy received an invalid directive value for "default-src"', 208 | }); 209 | }); 210 | 211 | describe("warnings", () => { 212 | const mockWarn = (t: TestContext) => 213 | t.mock.method(console, "warn", () => {}); 214 | 215 | const assertWarns = ( 216 | { mock }: Mock<() => unknown>, 217 | message: string, 218 | ): void => { 219 | assert.equal(mock.callCount(), 1); 220 | assert.deepEqual(mock.calls[0]?.arguments, [message]); 221 | }; 222 | 223 | it("logs a warning when passing options to xPoweredBy", (t) => { 224 | const warn = mockWarn(t); 225 | topLevel({ xPoweredBy: { setTo: "deprecated option" } as any }); 226 | assertWarns( 227 | warn, 228 | "X-Powered-By does not take options. Remove the property to silence this warning.", 229 | ); 230 | }); 231 | 232 | it("logs a warning when passing options to xDownloadOptions", (t) => { 233 | const warn = mockWarn(t); 234 | topLevel({ xDownloadOptions: { option: "foo" } as any }); 235 | assertWarns( 236 | warn, 237 | "X-Download-Options does not take options. Remove the property to silence this warning.", 238 | ); 239 | }); 240 | 241 | it("logs a warning when passing options to originAgentCluster", (t) => { 242 | const warn = mockWarn(t); 243 | topLevel({ originAgentCluster: { option: "foo" } as any }); 244 | assertWarns( 245 | warn, 246 | "Origin-Agent-Cluster does not take options. Remove the property to silence this warning.", 247 | ); 248 | }); 249 | 250 | it("logs a warning when passing options to xContentTypeOptions", (t) => { 251 | const warn = mockWarn(t); 252 | topLevel({ xContentTypeOptions: { option: "foo" } as any }); 253 | assertWarns( 254 | warn, 255 | "X-Content-Type-Options does not take options. Remove the property to silence this warning.", 256 | ); 257 | }); 258 | 259 | it("logs a warning when passing options to xXssProtection", (t) => { 260 | const warn = mockWarn(t); 261 | topLevel({ xXssProtection: { setOnOldIe: true } as any }); 262 | assertWarns( 263 | warn, 264 | "X-XSS-Protection does not take options. Remove the property to silence this warning.", 265 | ); 266 | }); 267 | }); 268 | 269 | it("exposes standalone middleware", () => { 270 | assert.strictEqual( 271 | helmet.contentSecurityPolicy.name, 272 | contentSecurityPolicy.name, 273 | ); 274 | assert.strictEqual( 275 | helmet.contentSecurityPolicy.name, 276 | "contentSecurityPolicy", 277 | ); 278 | 279 | assert.strictEqual( 280 | helmet.crossOriginEmbedderPolicy.name, 281 | crossOriginEmbedderPolicy.name, 282 | ); 283 | assert.strictEqual( 284 | helmet.crossOriginEmbedderPolicy.name, 285 | "crossOriginEmbedderPolicy", 286 | ); 287 | 288 | assert.strictEqual( 289 | helmet.crossOriginOpenerPolicy.name, 290 | crossOriginOpenerPolicy.name, 291 | ); 292 | assert.strictEqual( 293 | helmet.crossOriginOpenerPolicy.name, 294 | "crossOriginOpenerPolicy", 295 | ); 296 | 297 | assert.strictEqual( 298 | helmet.crossOriginResourcePolicy.name, 299 | crossOriginResourcePolicy.name, 300 | ); 301 | assert.strictEqual( 302 | helmet.crossOriginResourcePolicy.name, 303 | "crossOriginResourcePolicy", 304 | ); 305 | 306 | assert.strictEqual(helmet.originAgentCluster.name, originAgentCluster.name); 307 | assert.strictEqual(helmet.originAgentCluster.name, "originAgentCluster"); 308 | 309 | assert.strictEqual(helmet.referrerPolicy.name, referrerPolicy.name); 310 | assert.strictEqual(helmet.referrerPolicy.name, "referrerPolicy"); 311 | 312 | assert.strictEqual( 313 | helmet.strictTransportSecurity.name, 314 | strictTransportSecurity.name, 315 | ); 316 | assert.strictEqual( 317 | helmet.strictTransportSecurity.name, 318 | "strictTransportSecurity", 319 | ); 320 | 321 | assert.strictEqual( 322 | helmet.xContentTypeOptions.name, 323 | xContentTypeOptions.name, 324 | ); 325 | assert.strictEqual(helmet.xContentTypeOptions.name, "xContentTypeOptions"); 326 | 327 | assert.strictEqual( 328 | helmet.xDnsPrefetchControl.name, 329 | xDnsPrefetchControl.name, 330 | ); 331 | assert.strictEqual(helmet.xDnsPrefetchControl.name, "xDnsPrefetchControl"); 332 | 333 | assert.strictEqual(helmet.xDownloadOptions.name, xDownloadOptions.name); 334 | assert.strictEqual(helmet.xDownloadOptions.name, "xDownloadOptions"); 335 | 336 | assert.strictEqual(helmet.xFrameOptions.name, xFrameOptions.name); 337 | assert.strictEqual(helmet.xFrameOptions.name, "xFrameOptions"); 338 | 339 | assert.strictEqual( 340 | helmet.xPermittedCrossDomainPolicies.name, 341 | xPermittedCrossDomainPolicies.name, 342 | ); 343 | assert.strictEqual( 344 | helmet.xPermittedCrossDomainPolicies.name, 345 | "xPermittedCrossDomainPolicies", 346 | ); 347 | 348 | assert.strictEqual(helmet.xPoweredBy.name, xPoweredBy.name); 349 | assert.strictEqual(helmet.xPoweredBy.name, "xPoweredBy"); 350 | 351 | assert.strictEqual(helmet.xXssProtection.name, xXssProtection.name); 352 | assert.strictEqual(helmet.xXssProtection.name, "xXssProtection"); 353 | }); 354 | 355 | it("exposes legacy header options", async () => { 356 | await check(topLevel({ hsts: { maxAge: 123 } }), { 357 | "strict-transport-security": "max-age=123; includeSubDomains", 358 | }); 359 | await check(topLevel({ noSniff: false }), { 360 | "x-content-type-options": null, 361 | }); 362 | await check(topLevel({ dnsPrefetchControl: { allow: true } }), { 363 | "x-dns-prefetch-control": "on", 364 | }); 365 | await check(topLevel({ ieNoOpen: false }), { 366 | "x-download-options": null, 367 | }); 368 | await check(topLevel({ frameguard: { action: "deny" } }), { 369 | "x-frame-options": "DENY", 370 | }); 371 | await check( 372 | topLevel({ 373 | permittedCrossDomainPolicies: { permittedPolicies: "by-content-type" }, 374 | }), 375 | { 376 | "x-permitted-cross-domain-policies": "by-content-type", 377 | }, 378 | ); 379 | await check(topLevel({ hidePoweredBy: false }), { 380 | "x-powered-by": "Helmet test", 381 | }); 382 | await check(topLevel({ xssFilter: false }), { 383 | "x-xss-protection": null, 384 | }); 385 | }); 386 | 387 | it("errors with conflicting header options (legacy + new)", () => { 388 | assert.throws(() => 389 | topLevel({ strictTransportSecurity: true, hsts: true } as any), 390 | ); 391 | 392 | assert.throws(() => 393 | topLevel({ xContentTypeOptions: true, noSniff: true } as any), 394 | ); 395 | 396 | assert.throws(() => 397 | topLevel({ xDnsPrefetchControl: true, dnsPrefetchControl: true } as any), 398 | ); 399 | 400 | assert.throws(() => 401 | topLevel({ xDownloadOptions: true, ieNoOpen: true } as any), 402 | ); 403 | 404 | assert.throws(() => 405 | topLevel({ xFrameOptions: true, frameguard: true } as any), 406 | ); 407 | 408 | assert.throws(() => 409 | topLevel({ 410 | xPermittedCrossDomainPolicies: true, 411 | permittedCrossDomainPolicies: true, 412 | } as any), 413 | ); 414 | 415 | assert.throws(() => 416 | topLevel({ xPoweredBy: true, hidePoweredBy: true } as any), 417 | ); 418 | 419 | assert.throws(() => 420 | topLevel({ xXssProtection: true, xssFilter: true } as any), 421 | ); 422 | }); 423 | 424 | it("exposes standalone middleware with legacy aliases", () => { 425 | assert.strictEqual(helmet.hsts.name, strictTransportSecurity.name); 426 | assert.strictEqual( 427 | helmet.dnsPrefetchControl.name, 428 | xDnsPrefetchControl.name, 429 | ); 430 | assert.strictEqual(helmet.ieNoOpen.name, xDownloadOptions.name); 431 | assert.strictEqual(helmet.frameguard.name, xFrameOptions.name); 432 | assert.strictEqual(helmet.noSniff.name, xContentTypeOptions.name); 433 | assert.strictEqual(helmet.hidePoweredBy.name, xPoweredBy.name); 434 | assert.strictEqual( 435 | helmet.permittedCrossDomainPolicies.name, 436 | xPermittedCrossDomainPolicies.name, 437 | ); 438 | assert.strictEqual(helmet.xssFilter.name, xXssProtection.name); 439 | }); 440 | }); 441 | -------------------------------------------------------------------------------- /test/origin-agent-cluster.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import originAgentCluster from "../middlewares/origin-agent-cluster"; 3 | import { check } from "./helpers"; 4 | 5 | describe("Origin-Agent-Cluster middleware", () => { 6 | it('sets "Origin-Agent-Cluster: ?1"', async () => { 7 | await check(originAgentCluster(), { 8 | "origin-agent-cluster": "?1", 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/project-setups.test.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from "node:child_process"; 2 | import * as fs from "node:fs"; 3 | import * as path from "node:path"; 4 | import test from "node:test"; 5 | import { fileURLToPath } from "node:url"; 6 | import { promisify } from "node:util"; 7 | import { npm } from "../build/helpers.js"; 8 | 9 | const exec = promisify(childProcess.exec); 10 | 11 | const projectSetupsFolder = fileURLToPath( 12 | new URL("./project-setups", import.meta.url), 13 | ); 14 | const projectSetups = fs 15 | .readdirSync(projectSetupsFolder, { withFileTypes: true }) 16 | .filter((dirent) => dirent.isDirectory()) 17 | .map((dirent) => dirent.name); 18 | 19 | async function buildHelmet(): Promise { 20 | // Unfortunately, we can't import `buildAndPack` directly. 21 | // Run it and get the last line: the tarball path. 22 | const { stdout } = await exec("npm run build"); 23 | const lines = stdout.trim().split(/\r?\n/g); 24 | const result = lines[lines.length - 1]?.trim(); 25 | if (!result) { 26 | throw new Error("Couldn't parse tarball path from build output"); 27 | } 28 | return result; 29 | } 30 | 31 | let helmetTarballPromise: undefined | Promise; 32 | async function getHelmetTarballPath(): Promise { 33 | if (!helmetTarballPromise) { 34 | helmetTarballPromise = buildHelmet(); 35 | } 36 | return helmetTarballPromise; 37 | } 38 | 39 | for (const projectSetupName of projectSetups) { 40 | test(`${projectSetupName} project setup`, { timeout: 60_000 }, async () => { 41 | const projectFolder = path.join(projectSetupsFolder, projectSetupName); 42 | const nodeModulesFolder = path.join(projectFolder, "node_modules"); 43 | 44 | await fs.promises.rm(nodeModulesFolder, { recursive: true, force: true }); 45 | 46 | await npm( 47 | ["install", "--no-save", "--no-audit", await getHelmetTarballPath()], 48 | { 49 | cwd: projectFolder, 50 | }, 51 | ); 52 | 53 | await npm(["run", "helmet:test"], { cwd: projectFolder }); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/project-setups/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /test/project-setups/javascript-commonjs-default-member/check.js: -------------------------------------------------------------------------------- 1 | const connect = require("connect"); 2 | const supertest = require("supertest"); 3 | const { default: helmet, frameguard } = require("helmet"); 4 | 5 | const handler = (_, res) => res.end("Hello world"); 6 | 7 | async function testTopLevel() { 8 | const app = connect().use(helmet()).use(handler); 9 | await supertest(app) 10 | .get("/") 11 | .expect(200, "Hello world") 12 | .expect("x-download-options", "noopen"); 13 | } 14 | 15 | async function testMiddleware() { 16 | const app = connect().use(frameguard()).use(handler); 17 | await supertest(app) 18 | .get("/") 19 | .expect(200, "Hello world") 20 | .expect("x-frame-options", "SAMEORIGIN"); 21 | } 22 | 23 | async function main() { 24 | await testTopLevel(); 25 | await testMiddleware(); 26 | } 27 | 28 | main().catch((err) => { 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /test/project-setups/javascript-commonjs-default-member/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "commonjs", 4 | "scripts": { 5 | "helmet:test": "node check.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/javascript-commonjs/check.js: -------------------------------------------------------------------------------- 1 | const connect = require("connect"); 2 | const supertest = require("supertest"); 3 | const helmet = require("helmet"); 4 | 5 | const handler = (_, res) => res.end("Hello world"); 6 | 7 | async function testTopLevel() { 8 | const app = connect().use(helmet()).use(handler); 9 | await supertest(app) 10 | .get("/") 11 | .expect(200, "Hello world") 12 | .expect("x-download-options", "noopen"); 13 | } 14 | 15 | async function testMiddleware() { 16 | const app = connect().use(helmet.frameguard()).use(handler); 17 | await supertest(app) 18 | .get("/") 19 | .expect(200, "Hello world") 20 | .expect("x-frame-options", "SAMEORIGIN"); 21 | } 22 | 23 | async function main() { 24 | await testTopLevel(); 25 | await testMiddleware(); 26 | } 27 | 28 | main().catch((err) => { 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /test/project-setups/javascript-commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "commonjs", 4 | "scripts": { 5 | "helmet:test": "node check.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/javascript-esm/check.js: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import supertest from "supertest"; 4 | 5 | const handler = (_, res) => res.end("Hello world"); 6 | 7 | async function testTopLevel() { 8 | const app = connect().use(helmet()).use(handler); 9 | await supertest(app) 10 | .get("/") 11 | .expect(200, "Hello world") 12 | .expect("x-download-options", "noopen"); 13 | } 14 | 15 | async function testImportedMiddleware() { 16 | const app = connect().use(frameguard()).use(handler); 17 | await supertest(app) 18 | .get("/") 19 | .expect(200, "Hello world") 20 | .expect("x-frame-options", "SAMEORIGIN"); 21 | } 22 | 23 | async function testAttachedMiddleware() { 24 | const app = connect().use(helmet.frameguard()).use(handler); 25 | await supertest(app) 26 | .get("/") 27 | .expect(200, "Hello world") 28 | .expect("x-frame-options", "SAMEORIGIN"); 29 | } 30 | 31 | async function main() { 32 | await testTopLevel(); 33 | await testImportedMiddleware(); 34 | await testAttachedMiddleware(); 35 | } 36 | 37 | main().catch((err) => { 38 | console.error(err); 39 | process.exit(1); 40 | }); 41 | -------------------------------------------------------------------------------- /test/project-setups/javascript-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "helmet:test": "node check.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs-nodenext-moduleResolution/check.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | const handler = (_: IncomingMessage, res: ServerResponse) => { 7 | res.end("Hello world"); 8 | }; 9 | 10 | async function testTopLevel() { 11 | const app = connect().use(helmet()).use(handler); 12 | await supertest(app) 13 | .get("/") 14 | .expect(200, "Hello world") 15 | .expect("x-download-options", "noopen"); 16 | } 17 | 18 | async function testImportedMiddleware() { 19 | const app = connect().use(frameguard()).use(handler); 20 | await supertest(app) 21 | .get("/") 22 | .expect(200, "Hello world") 23 | .expect("x-frame-options", "SAMEORIGIN"); 24 | } 25 | 26 | async function testAttachedMiddleware() { 27 | const app = connect().use(helmet.frameguard()).use(handler); 28 | await supertest(app) 29 | .get("/") 30 | .expect(200, "Hello world") 31 | .expect("x-frame-options", "SAMEORIGIN"); 32 | } 33 | 34 | async function main() { 35 | await testTopLevel(); 36 | await testImportedMiddleware(); 37 | await testAttachedMiddleware(); 38 | } 39 | 40 | main().catch((err) => { 41 | console.error(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs-nodenext-moduleResolution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "commonjs", 4 | "scripts": { 5 | "helmet:test": "tsx check.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs-nodenext-moduleResolution/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "nodenext", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs/check.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | const handler = (_: IncomingMessage, res: ServerResponse) => { 7 | res.end("Hello world"); 8 | }; 9 | 10 | async function testTopLevel() { 11 | const app = connect().use(helmet()).use(handler); 12 | await supertest(app) 13 | .get("/") 14 | .expect(200, "Hello world") 15 | .expect("x-download-options", "noopen"); 16 | } 17 | 18 | async function testImportedMiddleware() { 19 | const app = connect().use(frameguard()).use(handler); 20 | await supertest(app) 21 | .get("/") 22 | .expect(200, "Hello world") 23 | .expect("x-frame-options", "SAMEORIGIN"); 24 | } 25 | 26 | async function testAttachedMiddleware() { 27 | const app = connect().use(helmet.frameguard()).use(handler); 28 | await supertest(app) 29 | .get("/") 30 | .expect(200, "Hello world") 31 | .expect("x-frame-options", "SAMEORIGIN"); 32 | } 33 | 34 | async function main() { 35 | await testTopLevel(); 36 | await testImportedMiddleware(); 37 | await testAttachedMiddleware(); 38 | } 39 | 40 | main().catch((err) => { 41 | console.error(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "commonjs", 4 | "scripts": { 5 | "helmet:test": "tsx check.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-commonjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-esnext/check.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | const handler = (_: IncomingMessage, res: ServerResponse) => { 7 | res.end("Hello world"); 8 | }; 9 | 10 | async function testTopLevel() { 11 | const app = connect().use(helmet()).use(handler); 12 | await supertest(app) 13 | .get("/") 14 | .expect(200, "Hello world") 15 | .expect("x-download-options", "noopen"); 16 | } 17 | 18 | async function testImportedMiddleware() { 19 | const app = connect().use(frameguard()).use(handler); 20 | await supertest(app) 21 | .get("/") 22 | .expect(200, "Hello world") 23 | .expect("x-frame-options", "SAMEORIGIN"); 24 | } 25 | 26 | async function testAttachedMiddleware() { 27 | const app = connect().use(helmet.frameguard()).use(handler); 28 | await supertest(app) 29 | .get("/") 30 | .expect(200, "Hello world") 31 | .expect("x-frame-options", "SAMEORIGIN"); 32 | } 33 | 34 | async function main() { 35 | await testTopLevel(); 36 | await testImportedMiddleware(); 37 | await testAttachedMiddleware(); 38 | } 39 | 40 | main().catch((err) => { 41 | console.error(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/project-setups/typescript-esnext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "helmet:test": "tsx check.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-esnext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-commonjs/check.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | const handler = (_: IncomingMessage, res: ServerResponse) => { 7 | res.end("Hello world"); 8 | }; 9 | 10 | async function testTopLevel() { 11 | const app = connect().use(helmet()).use(handler); 12 | await supertest(app) 13 | .get("/") 14 | .expect(200, "Hello world") 15 | .expect("x-download-options", "noopen"); 16 | } 17 | 18 | async function testImportedMiddleware() { 19 | const app = connect().use(frameguard()).use(handler); 20 | await supertest(app) 21 | .get("/") 22 | .expect(200, "Hello world") 23 | .expect("x-frame-options", "SAMEORIGIN"); 24 | } 25 | 26 | async function testAttachedMiddleware() { 27 | const app = connect().use(helmet.frameguard()).use(handler); 28 | await supertest(app) 29 | .get("/") 30 | .expect(200, "Hello world") 31 | .expect("x-frame-options", "SAMEORIGIN"); 32 | } 33 | 34 | async function main() { 35 | await testTopLevel(); 36 | await testImportedMiddleware(); 37 | await testAttachedMiddleware(); 38 | } 39 | 40 | main().catch((err) => { 41 | console.error(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "commonjs", 4 | "scripts": { 5 | "helmet:test": "tsx check.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-commonjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "moduleResolution": "nodenext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-esm/check.ts: -------------------------------------------------------------------------------- 1 | import connect from "connect"; 2 | import helmet, { frameguard } from "helmet"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import supertest from "supertest"; 5 | 6 | const handler = (_: IncomingMessage, res: ServerResponse) => { 7 | res.end("Hello world"); 8 | }; 9 | 10 | async function testTopLevel() { 11 | const app = connect().use(helmet()).use(handler); 12 | await supertest(app) 13 | .get("/") 14 | .expect(200, "Hello world") 15 | .expect("x-download-options", "noopen"); 16 | } 17 | 18 | async function testImportedMiddleware() { 19 | const app = connect().use(frameguard()).use(handler); 20 | await supertest(app) 21 | .get("/") 22 | .expect(200, "Hello world") 23 | .expect("x-frame-options", "SAMEORIGIN"); 24 | } 25 | 26 | async function testAttachedMiddleware() { 27 | const app = connect().use(helmet.frameguard()).use(handler); 28 | await supertest(app) 29 | .get("/") 30 | .expect(200, "Hello world") 31 | .expect("x-frame-options", "SAMEORIGIN"); 32 | } 33 | 34 | async function main() { 35 | await testTopLevel(); 36 | await testImportedMiddleware(); 37 | await testAttachedMiddleware(); 38 | } 39 | 40 | main().catch((err) => { 41 | console.error(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "helmet:test": "tsx check.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/project-setups/typescript-nodenext-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "moduleResolution": "nodenext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/referrer-policy.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import referrerPolicy from "../middlewares/referrer-policy"; 4 | import { check } from "./helpers"; 5 | 6 | describe("Referrer-Policy middleware", () => { 7 | it("sets header to no-referrer when passed no policy", async () => { 8 | await check(referrerPolicy(), { 9 | "referrer-policy": "no-referrer", 10 | }); 11 | await check(referrerPolicy({}), { 12 | "referrer-policy": "no-referrer", 13 | }); 14 | await check(referrerPolicy(Object.create(null)), { 15 | "referrer-policy": "no-referrer", 16 | }); 17 | await check(referrerPolicy({ policy: undefined }), { 18 | "referrer-policy": "no-referrer", 19 | }); 20 | }); 21 | 22 | ( 23 | [ 24 | "no-referrer", 25 | "no-referrer-when-downgrade", 26 | "same-origin", 27 | "origin", 28 | "strict-origin", 29 | "origin-when-cross-origin", 30 | "strict-origin-when-cross-origin", 31 | "unsafe-url", 32 | "", 33 | ] as const 34 | ).forEach((policy) => { 35 | it(`can set the header to "${policy}" by specifying it as a string`, async () => { 36 | await check(referrerPolicy({ policy }), { 37 | "referrer-policy": policy, 38 | }); 39 | }); 40 | 41 | it(`can set the header to "${policy}" by specifying it as an array string`, async () => { 42 | await check(referrerPolicy({ policy: [policy] }), { 43 | "referrer-policy": policy, 44 | }); 45 | }); 46 | }); 47 | 48 | it("can set an array with multiple values", async () => { 49 | await check(referrerPolicy({ policy: ["origin", "unsafe-url"] }), { 50 | "referrer-policy": "origin,unsafe-url", 51 | }); 52 | }); 53 | 54 | it("fails with a bad policy", () => { 55 | const invalidValues = ["foo", "sameorigin", "ORIGIN", 123, false, null, {}]; 56 | for (const policy of invalidValues) { 57 | assert.throws(() => referrerPolicy({ policy: policy as any })); 58 | } 59 | }); 60 | 61 | it("fails with an empty array", () => { 62 | assert.throws(() => referrerPolicy({ policy: [] })); 63 | }); 64 | 65 | it("fails with duplicate values", () => { 66 | assert.throws(() => referrerPolicy({ policy: ["origin", "origin"] })); 67 | assert.throws(() => 68 | referrerPolicy({ policy: ["same-origin", "origin", "same-origin"] }), 69 | ); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/source-files.test.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from "node:child_process"; 2 | import * as fs from "node:fs/promises"; 3 | import * as path from "node:path"; 4 | import { describe, it } from "node:test"; 5 | import { fileURLToPath } from "node:url"; 6 | import { promisify } from "node:util"; 7 | 8 | const EXTNAMES_THAT_DONT_HAVE_TO_BE_ASCII: ReadonlySet = new Set([ 9 | ".md", 10 | ]); 11 | const NEWLINE = "\n".charCodeAt(0); 12 | const SPACE = " ".charCodeAt(0); 13 | const TILDE = "~".charCodeAt(0); 14 | 15 | const exec = promisify(childProcess.exec); 16 | 17 | const filename = fileURLToPath(import.meta.url); 18 | const root = path.resolve(path.dirname(filename), ".."); 19 | 20 | describe("source files", () => { 21 | it('only has "normal" ASCII characters in the source files', async () => { 22 | const sourceFiles = await getSourceFiles(); 23 | for (const { path, contents } of sourceFiles) { 24 | const abnormalByteIndex = contents.findIndex( 25 | (byte) => !isNormalAsciiByte(byte), 26 | ); 27 | if (abnormalByteIndex !== -1) { 28 | throw new Error( 29 | `${path} must only contain "normal" ASCII characters but contained abnormal byte at ${abnormalByteIndex}`, 30 | ); 31 | } 32 | } 33 | }); 34 | }); 35 | 36 | const getSourceFiles = async (): Promise< 37 | Iterable<{ path: string; contents: Uint8Array }> 38 | > => { 39 | const paths = await getSourceFilePaths(); 40 | return Promise.all( 41 | paths.map(async (path) => ({ 42 | path, 43 | contents: await fs.readFile(path), 44 | })), 45 | ); 46 | }; 47 | 48 | const getSourceFilePaths = async (): Promise> => 49 | (await exec("git ls-files", { cwd: root })).stdout 50 | .split(/\r?\n/g) 51 | .filter( 52 | (file) => !EXTNAMES_THAT_DONT_HAVE_TO_BE_ASCII.has(path.extname(file)), 53 | ) 54 | .filter(Boolean) 55 | .map((line) => path.resolve(root, line)); 56 | 57 | const isNormalAsciiByte = (byte: number): boolean => 58 | byte === NEWLINE || (byte >= SPACE && byte <= TILDE); 59 | -------------------------------------------------------------------------------- /test/strict-transport-security.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import strictTransportSecurity from "../middlewares/strict-transport-security"; 4 | import { check } from "./helpers"; 5 | 6 | describe("Strict-Transport-Security middleware", () => { 7 | it('by default, sets max-age to 365 days and adds "includeSubDomains"', async () => { 8 | assert.equal(31536000, 365 * 24 * 60 * 60); 9 | 10 | const expectedHeaders = { 11 | "strict-transport-security": "max-age=31536000; includeSubDomains", 12 | }; 13 | 14 | await check(strictTransportSecurity(), expectedHeaders); 15 | await check(strictTransportSecurity({}), expectedHeaders); 16 | await check(strictTransportSecurity(Object.create(null)), expectedHeaders); 17 | await check( 18 | strictTransportSecurity({ maxAge: undefined }), 19 | expectedHeaders, 20 | ); 21 | await check( 22 | strictTransportSecurity({ includeSubDomains: undefined }), 23 | expectedHeaders, 24 | ); 25 | }); 26 | 27 | it("sets the max-age to a non-negative integer", async () => { 28 | await check(strictTransportSecurity({ maxAge: 1234 }), { 29 | "strict-transport-security": "max-age=1234; includeSubDomains", 30 | }); 31 | await check(strictTransportSecurity({ maxAge: 0 }), { 32 | "strict-transport-security": "max-age=0; includeSubDomains", 33 | }); 34 | await check(strictTransportSecurity({ maxAge: -0 }), { 35 | "strict-transport-security": "max-age=0; includeSubDomains", 36 | }); 37 | }); 38 | 39 | it("rounds non-integer max-ages down", async () => { 40 | await check(strictTransportSecurity({ maxAge: 123.4 }), { 41 | "strict-transport-security": "max-age=123; includeSubDomains", 42 | }); 43 | await check(strictTransportSecurity({ maxAge: 123.5 }), { 44 | "strict-transport-security": "max-age=123; includeSubDomains", 45 | }); 46 | }); 47 | 48 | it("disables subdomains with the includeSubDomains option", async () => { 49 | await check(strictTransportSecurity({ includeSubDomains: false }), { 50 | "strict-transport-security": "max-age=31536000", 51 | }); 52 | }); 53 | 54 | it("can enable preloading", async () => { 55 | await check(strictTransportSecurity({ preload: true }), { 56 | "strict-transport-security": 57 | "max-age=31536000; includeSubDomains; preload", 58 | }); 59 | }); 60 | 61 | it("can explicitly disable preloading", async () => { 62 | await check(strictTransportSecurity({ preload: false }), { 63 | "strict-transport-security": "max-age=31536000; includeSubDomains", 64 | }); 65 | }); 66 | 67 | it("throws an error with invalid parameters", () => { 68 | assert.throws(() => strictTransportSecurity({ maxAge: -123 })); 69 | assert.throws(() => 70 | strictTransportSecurity({ maxAge: BigInt(-123) as any }), 71 | ); 72 | assert.throws(() => strictTransportSecurity({ maxAge: -0.1 })); 73 | assert.throws(() => strictTransportSecurity({ maxAge: Infinity })); 74 | assert.throws(() => strictTransportSecurity({ maxAge: -Infinity })); 75 | assert.throws(() => strictTransportSecurity({ maxAge: NaN })); 76 | 77 | assert.throws(() => strictTransportSecurity({ maxAge: "123" } as any)); 78 | assert.throws(() => 79 | strictTransportSecurity({ maxAge: BigInt(123) } as any), 80 | ); 81 | assert.throws(() => strictTransportSecurity({ maxAge: true } as any)); 82 | assert.throws(() => strictTransportSecurity({ maxAge: false } as any)); 83 | assert.throws(() => strictTransportSecurity({ maxAge: {} } as any)); 84 | assert.throws(() => strictTransportSecurity({ maxAge: [] } as any)); 85 | assert.throws(() => strictTransportSecurity({ maxAge: null } as any)); 86 | 87 | assert.throws(() => strictTransportSecurity({ maxage: false } as any)); 88 | assert.throws(() => strictTransportSecurity({ maxage: 1234 } as any)); 89 | }); 90 | 91 | it("logs a warning when using the mis-capitalized `includeSubdomains` parameter", () => { 92 | assert.throws( 93 | () => strictTransportSecurity({ includeSubdomains: false } as any), 94 | { 95 | message: 96 | 'Strict-Transport-Security middleware should use `includeSubDomains` instead of `includeSubdomains`. (The correct one has an uppercase "D".)', 97 | }, 98 | ); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/x-content-type-options.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import xContentTypeOptions from "../middlewares/x-content-type-options"; 3 | import { check } from "./helpers"; 4 | 5 | describe("X-Content-Type-Options middleware", () => { 6 | it('sets "X-Content-Type-Options: nosniff"', async () => { 7 | await check(xContentTypeOptions(), { 8 | "x-content-type-options": "nosniff", 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/x-dns-prefetch-control.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import xDnsPrefetchControl from "../middlewares/x-dns-prefetch-control"; 3 | import { check } from "./helpers"; 4 | 5 | describe("X-DNS-Prefetch-Control middleware", () => { 6 | it('sets the header to "off" by default', async () => { 7 | const expectedHeaders = { "x-dns-prefetch-control": "off" }; 8 | 9 | await check(xDnsPrefetchControl(), expectedHeaders); 10 | await check(xDnsPrefetchControl({}), expectedHeaders); 11 | await check(xDnsPrefetchControl(Object.create(null)), expectedHeaders); 12 | }); 13 | 14 | it('can set header to "off" with configuration', async () => { 15 | await check(xDnsPrefetchControl({ allow: false }), { 16 | "x-dns-prefetch-control": "off", 17 | }); 18 | }); 19 | 20 | it('can set header to "on" with configuration', async () => { 21 | await check(xDnsPrefetchControl({ allow: true }), { 22 | "x-dns-prefetch-control": "on", 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/x-download-options.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import xDownloadOptions from "../middlewares/x-download-options"; 3 | import { check } from "./helpers"; 4 | 5 | describe("X-Download-Options middleware", () => { 6 | it('sets "X-Download-Options: noopen"', async () => { 7 | await check(xDownloadOptions(), { 8 | "x-download-options": "noopen", 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/x-frame-options.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import xFrameOptions from "../middlewares/x-frame-options"; 4 | import { check } from "./helpers"; 5 | 6 | describe("X-Frame-Options middleware", () => { 7 | it('sets "X-Frame-Options: SAMEORIGIN" when passed no action', async () => { 8 | await check(xFrameOptions(), { 9 | "x-frame-options": "SAMEORIGIN", 10 | }); 11 | await check(xFrameOptions({}), { 12 | "x-frame-options": "SAMEORIGIN", 13 | }); 14 | await check(xFrameOptions(Object.create(null)), { 15 | "x-frame-options": "SAMEORIGIN", 16 | }); 17 | await check(xFrameOptions({ action: undefined }), { 18 | "x-frame-options": "SAMEORIGIN", 19 | }); 20 | }); 21 | 22 | it('can set "X-Frame-Options: DENY"', async () => { 23 | await check(xFrameOptions({ action: "deny" }), { 24 | "x-frame-options": "DENY", 25 | }); 26 | 27 | // These are not allowed by the types, but are supported. 28 | await check(xFrameOptions({ action: "DENY" as any }), { 29 | "x-frame-options": "DENY", 30 | }); 31 | await check(xFrameOptions({ action: "deNY" as any }), { 32 | "x-frame-options": "DENY", 33 | }); 34 | }); 35 | 36 | it('can set "X-Frame-Options: SAMEORIGIN" when specified', async () => { 37 | await check(xFrameOptions({ action: "sameorigin" }), { 38 | "x-frame-options": "SAMEORIGIN", 39 | }); 40 | 41 | // These are not allowed by the types, but are supported. 42 | await check(xFrameOptions({ action: "SAMEORIGIN" as any }), { 43 | "x-frame-options": "SAMEORIGIN", 44 | }); 45 | await check(xFrameOptions({ action: "sameORIGIN" as any }), { 46 | "x-frame-options": "SAMEORIGIN", 47 | }); 48 | await check(xFrameOptions({ action: "SAME-ORIGIN" as any }), { 49 | "x-frame-options": "SAMEORIGIN", 50 | }); 51 | await check(xFrameOptions({ action: "same-origin" as any }), { 52 | "x-frame-options": "SAMEORIGIN", 53 | }); 54 | }); 55 | 56 | it("throws when passed invalid actions", () => { 57 | for (const action of [ 58 | "", 59 | "foo", 60 | " deny", 61 | "allow-from", 62 | "ALLOW-FROM", 63 | 123, 64 | null, 65 | new String("SAMEORIGIN"), 66 | ]) { 67 | assert.throws(() => xFrameOptions({ action: action as any }), { 68 | message: /^X-Frame-Options received an invalid action /, 69 | }); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/x-permitted-cross-domain-policies.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import xPermittedCrossDomainPolicies from "../middlewares/x-permitted-cross-domain-policies"; 4 | import { check } from "./helpers"; 5 | 6 | describe("X-Permitted-Cross-Domain-Policies middleware", () => { 7 | it('sets "X-Permitted-Cross-Domain-Policies: none" when called with no permitted policies', async () => { 8 | const expectedHeaders = { 9 | "x-permitted-cross-domain-policies": "none", 10 | }; 11 | await check(xPermittedCrossDomainPolicies(), expectedHeaders); 12 | await check(xPermittedCrossDomainPolicies({}), expectedHeaders); 13 | await check( 14 | xPermittedCrossDomainPolicies(Object.create(null)), 15 | expectedHeaders, 16 | ); 17 | await check( 18 | xPermittedCrossDomainPolicies({ permittedPolicies: undefined }), 19 | expectedHeaders, 20 | ); 21 | }); 22 | 23 | (["none", "master-only", "by-content-type", "all"] as const).forEach( 24 | (permittedPolicies) => { 25 | it(`sets "X-Permitted-Cross-Domain-Policies: ${permittedPolicies}" when told to`, async () => { 26 | await check(xPermittedCrossDomainPolicies({ permittedPolicies }), { 27 | "x-permitted-cross-domain-policies": permittedPolicies, 28 | }); 29 | }); 30 | }, 31 | ); 32 | 33 | it("throws when setting the policy to an invalid value", () => { 34 | const invalidValues = [ 35 | "", 36 | "NONE", 37 | "by-ftp-filename", 38 | 123, 39 | null, 40 | new String("none"), 41 | ]; 42 | for (const permittedPolicies of invalidValues) { 43 | assert.throws( 44 | () => 45 | xPermittedCrossDomainPolicies({ 46 | permittedPolicies: permittedPolicies as any, 47 | }), 48 | { message: /^X-Permitted-Cross-Domain-Policies does not support / }, 49 | ); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/x-powered-by.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import xPoweredBy from "../middlewares/x-powered-by"; 3 | import { check } from "./helpers"; 4 | 5 | describe("X-Powered-By middleware", () => { 6 | it("does nothing if the request was not set earlier in the stack", async () => { 7 | await check(xPoweredBy(), { 8 | "x-powered-by": null, 9 | }); 10 | }); 11 | 12 | it("removes the header if it was set earlier in the stack", async () => { 13 | await check( 14 | (req, res, next) => { 15 | res.setHeader("X-POWERED-BY", "should be destroyed"); 16 | xPoweredBy()(req, res, next); 17 | }, 18 | { 19 | "x-powered-by": null, 20 | }, 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/x-xss-protection.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import xXssProtection from "../middlewares/x-xss-protection"; 3 | import { check } from "./helpers"; 4 | 5 | describe("X-XSS-Protection middleware", () => { 6 | it('sets "X-XSS-Protection: 0"', async () => { 7 | await check(xXssProtection(), { 8 | "x-xss-protection": "0", 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noUncheckedSideEffectImports": true, 12 | "strict": true, 13 | "target": "es2022", 14 | "outDir": "." 15 | }, 16 | "exclude": ["node_modules", "test/project-setups"] 17 | } 18 | --------------------------------------------------------------------------------