Skip to content

Commit

Permalink
Import Strict-Transport-Security (hsts) middleware
Browse files Browse the repository at this point in the history
This imports the [hsts package][0] into this repo as part of my effort
to make Helmet a monorepo. You can find its prior history in the old
repo.

Similar to:

* f98ff72 which imported
  `x-xss-protection`
* df561bb which imported `helmet-csp`
* 936cd27 which imported
  `referrer-policy`
* 141f131 which imported `crossdomain`
* ff12fb7 which imported
  `dont-sniff-mimetype`
* 2b64d11 which imported
  `hide-powered-by`
* 7906601 which imported `frameguard`
* d03c555 which imported `expect-ct`
* e933c28 which imported
  `dns-prefetch-control`
* 13b496f which imported `ienoopen`

[0]: https://github.com/helmetjs/hsts
  • Loading branch information
EvanHahn committed Jul 10, 2020
1 parent f98ff72 commit 788d69b
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
- Removed the `disableAndroid` option
- `helmet.frameguard`:
- 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)
- `helmet.hsts`:
- Dropped support for `includeSubdomains` with a lowercase D. See [#231](https://github.com/helmetjs/helmet/issues/231)
- 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)
- `helmet.xssFilter` 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.

## Unreleased
Expand Down
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IncomingMessage, ServerResponse } from "http";
import contentSecurityPolicy from "./middlewares/content-security-policy";
import expectCt from "./middlewares/expect-ct";
import referrerPolicy from "./middlewares/referrer-policy";
import strictTransportSecurity from "./middlewares/strict-transport-security";
import xContentTypeOptions from "./middlewares/x-content-type-options";
import xDnsPrefetchControl from "./middlewares/x-dns-prefetch-control";
import xDownloadOptions from "./middlewares/x-download-options";
Expand Down Expand Up @@ -127,7 +128,7 @@ helmet.dnsPrefetchControl = xDnsPrefetchControl;
helmet.expectCt = expectCt;
helmet.frameguard = xFrameOptions;
helmet.hidePoweredBy = xPoweredBy;
helmet.hsts = require("hsts");
helmet.hsts = strictTransportSecurity;
helmet.ieNoOpen = xDownloadOptions;
helmet.noSniff = xContentTypeOptions;
helmet.permittedCrossDomainPolicies = xPermittedCrossDomainPolicies;
Expand Down
10 changes: 8 additions & 2 deletions middlewares/expect-ct/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ export interface ExpectCtOptions {
function parseMaxAge(value: void | number): number {
if (value === undefined) {
return 0;
} else if (typeof value === "number" && value >= 0) {
} else if (
typeof value === "number" &&
value >= 0 &&
Number.isFinite(value)
) {
return Math.floor(value);
} else {
throw new Error(
`${value} is not a valid value for maxAge. Please choose a positive integer.`
`Expect-CT: ${JSON.stringify(
value
)} is not a valid value for maxAge. Please choose a positive integer.`
);
}
}
Expand Down
34 changes: 34 additions & 0 deletions middlewares/strict-transport-security/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

## 3.0.0 - Unreleased

### Added

- TypeScript type definitions. See [#25](https://github.com/helmetjs/hsts/pull/25)

### Removed

- Dropped support for `includeSubdomains` with a lowercase D. See [#231](https://github.com/helmetjs/helmet/issues/231)
- 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)
- Dropped support for old Node versions. Node 10+ is now required

## 2.2.0 - 2019-03-10

### Added

- Created a changelog

### Changed

- Mark the module as Node 4+ in the `engines` field of `package.json`
- Add a `homepage` in `package.json`
- Add an email to `package.json`'s `bugs` field
- Updated documentation
- Updated Adam Baldwin's contact info. See [helmetjs/helmet#189](https://github.com/helmetjs/helmet/issues/189)

### Deprecated

- 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
- 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

Changes in versions 2.1.0 and below can be found in [Helmet's changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md).
45 changes: 45 additions & 0 deletions middlewares/strict-transport-security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# HTTP Strict Transport Security middleware

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](http://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.

This will set the Strict Transport Security header, telling browsers to visit by HTTPS for the next 180 days:

```javascript
const strictTransportSecurity = require("hsts");

// Sets "Strict-Transport-Security: max-age=15552000; includeSubDomains"
app.use(
strictTransportSecurity({
maxAge: 15552000, // 180 days in seconds
})
);
```

Note that the max age must be in seconds.

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:

```javascript
app.use(
strictTransportSecurity({
maxAge: 15552000,
includeSubDomains: false,
})
);
```

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/).

```javascript
app.use(
strictTransportSecurity({
maxAge: 31536000, // Must be at least 1 year to be approved
includeSubDomains: true, // Must be enabled to be approved
preload: true,
})
);
```

[The header is ignored in insecure HTTP](https://tools.ietf.org/html/rfc6797#section-8.1), so it's safe to set in development.

This header is [somewhat well-supported by browsers](https://caniuse.com/#feat=stricttransportsecurity).
77 changes: 77 additions & 0 deletions middlewares/strict-transport-security/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { IncomingMessage, ServerResponse } from "http";

const DEFAULT_MAX_AGE = 180 * 24 * 60 * 60;

export interface StrictTransportSecurityOptions {
maxAge?: number;
includeSubDomains?: boolean;
preload?: boolean;
}

function parseMaxAge(value: void | number): number {
if (value === undefined) {
return DEFAULT_MAX_AGE;
} else if (
typeof value === "number" &&
value >= 0 &&
Number.isFinite(value)
) {
return Math.floor(value);
} else {
throw new Error(
`Strict-Transport-Security: ${JSON.stringify(
value
)} is not a valid value for maxAge. Please choose a positive integer.`
);
}
}

function getHeaderValueFromOptions(
options: Readonly<StrictTransportSecurityOptions>
): string {
if ("maxage" in options) {
throw new Error(
'maxage is not a supported property. Did you mean to pass "maxAge" instead of "maxage"?'
);
}
if ("includeSubdomains" in options) {
console.warn(
'Strict-Transport-Security middleware should use `includeSubDomains` instead of `includeSubdomains`. (The correct one has an uppercase "D".)'
);
}
if ("setIf" in options) {
console.warn(
"Strict-Transport-Security middleware no longer supports the `setIf` parameter. See the documentation and <https://github.com/helmetjs/helmet/wiki/Conditionally-using-middleware> if you need help replicating this behavior."
);
}

const directives: string[] = [`max-age=${parseMaxAge(options.maxAge)}`];

if (options.includeSubDomains === undefined || options.includeSubDomains) {
directives.push("includeSubDomains");
}

if (options.preload) {
directives.push("preload");
}

return directives.join("; ");
}

function strictTransportSecurity(
options: Readonly<StrictTransportSecurityOptions> = {}
) {
const headerValue = getHeaderValueFromOptions(options);

return function strictTransportSecurityMiddleware(
_req: IncomingMessage,
res: ServerResponse,
next: () => void
) {
res.setHeader("Strict-Transport-Security", headerValue);
next();
};
}

module.exports = strictTransportSecurity;
export default strictTransportSecurity;
1 change: 1 addition & 0 deletions middlewares/strict-transport-security/package-files.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["index.js", "index.d.ts"]
13 changes: 13 additions & 0 deletions middlewares/strict-transport-security/package-overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "hsts",
"description": "HTTP Strict Transport Security middleware",
"version": "2.2.0",
"keywords": [
"express",
"security",
"hsts",
"strict-transport-security",
"https"
],
"homepage": "https://helmetjs.github.io/docs/hsts/"
}
13 changes: 0 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dist/middlewares/content-security-policy/index.js",
"dist/middlewares/expect-ct/index.js",
"dist/middlewares/referrer-policy/index.js",
"dist/middlewares/strict-transport-security/index.js",
"dist/middlewares/x-content-type-options/index.js",
"dist/middlewares/x-dns-prefetch-control/index.js",
"dist/middlewares/x-download-options/index.js",
Expand All @@ -49,9 +50,7 @@
"dist/middlewares/x-powered-by/index.js",
"dist/middlewares/x-xss-protection/index.js"
],
"dependencies": {
"hsts": "2.2.0"
},
"dependencies": {},
"devDependencies": {
"@types/connect": "^3.4.33",
"@types/jest": "^26.0.3",
Expand Down
6 changes: 5 additions & 1 deletion test/expect-ct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ describe("Expect-CT middleware", () => {
});
});

it("rejects negative max-ages", async () => {
it("rejects invalid max-ages", async () => {
expect(() => expectCt({ maxAge: -123 })).toThrow();
expect(() => expectCt({ maxAge: -0.1 })).toThrow();
expect(() => expectCt({ maxAge: Infinity })).toThrow();
expect(() => expectCt({ maxAge: NaN })).toThrow();
expect(() => expectCt({ maxAge: "123" as any })).toThrow();
expect(() => expectCt({ maxAge: BigInt(12) as any })).toThrow();
});

it("can enable enforcement", async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import helmet = require("..");
import contentSecurityPolicy from "../middlewares/content-security-policy";
import expectCt from "../middlewares/expect-ct";
import referrerPolicy from "../middlewares/referrer-policy";
import strictTransportSecurity from "../middlewares/strict-transport-security";
import xContentTypeOptions from "../middlewares/x-content-type-options";
import xDnsPrefetchControl from "../middlewares/x-dns-prefetch-control";
import xDowloadOptions from "../middlewares/x-download-options";
Expand Down Expand Up @@ -45,9 +46,8 @@ describe("helmet", function () {
expect(helmet.hidePoweredBy.name).toBe(xPoweredBy.name);
});

it('aliases "hsts"', function () {
const pkg = require("hsts");
expect(helmet.hsts).toBe(pkg);
it("aliases the Strict-Transport-Security middleware to helmet.hsts", function () {
expect(helmet.hsts.name).toBe(strictTransportSecurity.name);
});

it("aliases the X-Download-Options middleware to helmet.ieNoOpen", () => {
Expand Down
Loading

0 comments on commit 788d69b

Please sign in to comment.