Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions vendor/wheels/middleware/SecurityHeaders.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ component implements="wheels.middleware.MiddlewareInterface" output="false" {
* @xssProtection X-XSS-Protection value.
* @referrerPolicy Referrer-Policy value.
* @contentSecurityPolicy Content-Security-Policy value. Empty by default (opt-in) because a restrictive policy can break apps with inline scripts/styles.
* @strictTransportSecurity Strict-Transport-Security value. Empty by default (opt-in) because it requires HTTPS to be configured.
* @strictTransportSecurity Strict-Transport-Security value. Auto-defaults to `max-age=31536000; includeSubDomains` in production when not explicitly set.
* @permissionsPolicy Permissions-Policy value. Empty by default (opt-in) because it is app-specific.
* @environment Application environment (e.g. "production", "development"). When empty, falls back to application.$wheels.environment if available.
*/
public SecurityHeaders function init(
string frameOptions = "SAMEORIGIN",
Expand All @@ -26,9 +27,29 @@ component implements="wheels.middleware.MiddlewareInterface" output="false" {
string referrerPolicy = "strict-origin-when-cross-origin",
string contentSecurityPolicy = "",
string strictTransportSecurity = "",
string permissionsPolicy = ""
string permissionsPolicy = "",
string environment = ""
) {
variables.headers = {};

// Resolve environment: explicit parameter > application.$wheels.environment
local.env = arguments.environment;
if (!Len(local.env)) {
try {
if (StructKeyExists(application, "$wheels") && StructKeyExists(application.$wheels, "environment")) {
local.env = application.$wheels.environment;
}
} catch (any e) {
// application scope may not be available during testing
}
}

// Default HSTS in production when not explicitly configured
local.hsts = arguments.strictTransportSecurity;
if (!Len(local.hsts) && local.env == "production") {
local.hsts = "max-age=31536000; includeSubDomains";
}

if (Len(arguments.frameOptions)) {
variables.headers["X-Frame-Options"] = arguments.frameOptions;
}
Expand All @@ -44,8 +65,8 @@ component implements="wheels.middleware.MiddlewareInterface" output="false" {
if (Len(arguments.contentSecurityPolicy)) {
variables.headers["Content-Security-Policy"] = arguments.contentSecurityPolicy;
}
if (Len(arguments.strictTransportSecurity)) {
variables.headers["Strict-Transport-Security"] = arguments.strictTransportSecurity;
if (Len(local.hsts)) {
variables.headers["Strict-Transport-Security"] = local.hsts;
}
if (Len(arguments.permissionsPolicy)) {
variables.headers["Permissions-Policy"] = arguments.permissionsPolicy;
Expand Down
41 changes: 39 additions & 2 deletions vendor/wheels/tests/specs/middleware/SecurityHeadersSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ component extends="wheels.WheelsTest" {
expect(local.mw.$headers()).notToHaveKey("Content-Security-Policy");
});

it("does not set HSTS by default", function() {
it("does not set HSTS by default when environment is not production", function() {
local.mw = new wheels.middleware.SecurityHeaders();
expect(local.mw.$headers()).notToHaveKey("Strict-Transport-Security");
});
Expand Down Expand Up @@ -86,13 +86,50 @@ component extends="wheels.WheelsTest" {
expect(local.mw.$headers()["Strict-Transport-Security"]).toBe(local.value);
});

it("does not set HSTS header when empty string", function() {
it("does not set HSTS header when empty string and not production", function() {
local.mw = new wheels.middleware.SecurityHeaders(
strictTransportSecurity = ""
);
expect(local.mw.$headers()).notToHaveKey("Strict-Transport-Security");
});

it("auto-defaults HSTS in production environment", function() {
local.mw = new wheels.middleware.SecurityHeaders(
environment = "production"
);
expect(local.mw.$headers()).toHaveKey("Strict-Transport-Security");
expect(local.mw.$headers()["Strict-Transport-Security"]).toBe("max-age=31536000; includeSubDomains");
});

it("does not auto-default HSTS in development environment", function() {
local.mw = new wheels.middleware.SecurityHeaders(
environment = "development"
);
expect(local.mw.$headers()).notToHaveKey("Strict-Transport-Security");
});

it("does not auto-default HSTS in testing environment", function() {
local.mw = new wheels.middleware.SecurityHeaders(
environment = "testing"
);
expect(local.mw.$headers()).notToHaveKey("Strict-Transport-Security");
});

it("uses explicit HSTS value even in production", function() {
local.mw = new wheels.middleware.SecurityHeaders(
strictTransportSecurity = "max-age=86400",
environment = "production"
);
expect(local.mw.$headers()["Strict-Transport-Security"]).toBe("max-age=86400");
});

it("does not set HSTS when environment is empty and no explicit value", function() {
local.mw = new wheels.middleware.SecurityHeaders(
environment = ""
);
expect(local.mw.$headers()).notToHaveKey("Strict-Transport-Security");
});

});

describe("Permissions-Policy", function() {
Expand Down
Loading