From 2bdae3e0af57aba721f4a67de8f3a4ecdbc7ebd0 Mon Sep 17 00:00:00 2001 From: Dan Hudson Date: Thu, 14 Aug 2025 16:17:44 +0100 Subject: [PATCH 01/11] New: added class for adding security headers --- includes/classes/Security/Headers.php | 47 +++++++++++++++++++++++++++ orbit.php | 1 + 2 files changed, 48 insertions(+) create mode 100644 includes/classes/Security/Headers.php diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php new file mode 100644 index 0000000..a91ded7 --- /dev/null +++ b/includes/classes/Security/Headers.php @@ -0,0 +1,47 @@ + 'SAMEORIGIN', // Prevent clickjacking + ]; + + $security_headers = apply_filters( 'orbit_default_security_headers', $default_security_headers ); + + foreach ( $security_headers as $header => $value ) { + if ( ! empty( $value ) ) { + $headers[ $header ] = $value; + } + } + + return $headers; + } +} diff --git a/orbit.php b/orbit.php index f52e9f4..9d3a783 100644 --- a/orbit.php +++ b/orbit.php @@ -42,6 +42,7 @@ function () { Capabilities\Editor::instance()->setup(); Security\DisableAPI::instance()->setup(); Security\DisableXMLRPC::instance()->setup(); + Security\Headers::instance()->setup(); Security\HideAuthor::instance()->setup(); Security\HideVersion::instance()->setup(); Security\RemoveHeadLinks::instance()->setup(); From 1577654c827e628301089aeaa35fe8ebf8cd42d6 Mon Sep 17 00:00:00 2001 From: Dan Hudson Date: Thu, 14 Aug 2025 16:21:24 +0100 Subject: [PATCH 02/11] New: document security headers --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b32920..bbc378e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If necessary, you may install it manually by downloading a Zip archive from [Git - REST API links - Oembed links - Windows Live Writer manifest links +- Set sensible security headers ### Capabilities @@ -95,6 +96,7 @@ The following filters can be used to override the default behavior of certain fe - `orbit_enable_rest_api_user_endpoints`: Enable or disable REST API user endpoints. Default `false` (disabled). - `orbit_enable_xmlrpc`: Enable or disable XML-RPC functionality. Default `false` (disabled). - `orbit_enable_expose_wordpress_version`: Show or hide the WordPress version in the site's frontend markup. Default `false` (hidden). +- `orbit_default_security_headers`: Set an array of security headers. ### Capabilities From 4c69cf501534a1f20632d5ecc4f64be6acdceead Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Tue, 19 Aug 2025 22:21:22 +0100 Subject: [PATCH 03/11] Additional default security headers --- includes/classes/Security/Headers.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index a91ded7..4c6b04f 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -31,9 +31,31 @@ public function setup() { public function set_security_headers( $headers ) { $default_security_headers = [ - 'X-Frame-Options' => 'SAMEORIGIN', // Prevent clickjacking + // Cross-origin hardening + 'Cross-Origin-Opener-Policy' => 'same-origin', + 'Cross-Origin-Resource-Policy' => 'same-origin', + + // Sensible privacy default + 'Referrer-Policy' => 'strict-origin-when-cross-origin', + + // Stops MIME sniffing + 'X-Content-Type-Options' => 'nosniff', + + // Prevent clickjacking inside iframes (legacy) + 'X-Frame-Options' => 'SAMEORIGIN', ]; + $default_csp = [ + 'upgrade-insecure-requests', + "default-src 'self'", + ]; + $default_security_headers['Content-Security-Policy'] = trim( implode( '; ', $default_csp ) ); + + // Only if SSL + if ( is_ssl() ) { + $default_security_headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'; + } + $security_headers = apply_filters( 'orbit_default_security_headers', $default_security_headers ); foreach ( $security_headers as $header => $value ) { From b95da94c70c131bc84c8bddfd03e52b83aa5573c Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Mon, 10 Nov 2025 15:16:14 +0000 Subject: [PATCH 04/11] Set Cache-Control by default This is expected for PCI tests --- includes/classes/Security/Headers.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 4c6b04f..45c85d5 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -43,6 +43,9 @@ public function set_security_headers( $headers ) { // Prevent clickjacking inside iframes (legacy) 'X-Frame-Options' => 'SAMEORIGIN', + + // Good default for non-sensitive resources + 'Cache-Control' => 'Cache-Control: no-cache', ]; $default_csp = [ From 3435d8c2ef648d711342520858dc66587a904883 Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Mon, 10 Nov 2025 15:20:25 +0000 Subject: [PATCH 05/11] Typo correction in Headers.php --- includes/classes/Security/Headers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 45c85d5..f431e3c 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -45,7 +45,7 @@ public function set_security_headers( $headers ) { 'X-Frame-Options' => 'SAMEORIGIN', // Good default for non-sensitive resources - 'Cache-Control' => 'Cache-Control: no-cache', + 'Cache-Control' => 'no-cache', ]; $default_csp = [ From 10f5d2d0cab10fc25b7f51fd8b57c5cebe211ef0 Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Mon, 10 Nov 2025 16:30:57 +0000 Subject: [PATCH 06/11] Use a more permissive CSP by default This can be considered a placeholder for websites to customise based on their own needs. --- includes/classes/Security/Headers.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index f431e3c..265f82f 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -46,17 +46,15 @@ public function set_security_headers( $headers ) { // Good default for non-sensitive resources 'Cache-Control' => 'no-cache', - ]; - $default_csp = [ - 'upgrade-insecure-requests', - "default-src 'self'", + // Permissive CSP (websites should customise this, ideally) + 'Content-Security-Policy' => "default-src 'self' * 'unsafe-inline' 'unsafe-eval' data: blob:", ]; - $default_security_headers['Content-Security-Policy'] = trim( implode( '; ', $default_csp ) ); // Only if SSL if ( is_ssl() ) { $default_security_headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'; + $default_security_headers['Content-Security-Policy'] .= '; upgrade-insecure-requests'; } $security_headers = apply_filters( 'orbit_default_security_headers', $default_security_headers ); From a511c39599f5a5564bbb199d3834096a55c20b7f Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Sat, 15 Nov 2025 15:40:37 +0000 Subject: [PATCH 07/11] Package updates --- composer.lock | 77 ++++++++++--------- .../Component/CssSelector/Parser/Token.php | 8 +- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/composer.lock b/composer.lock index 90d4af2..2fb36cb 100644 --- a/composer.lock +++ b/composer.lock @@ -168,16 +168,16 @@ }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "84321188c4754e64273b46b406081ad9b18e8614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/84321188c4754e64273b46b406081ad9b18e8614", + "reference": "84321188c4754e64273b46b406081ad9b18e8614", "shasum": "" }, "require": { @@ -213,7 +213,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v7.3.6" }, "funding": [ { @@ -224,12 +224,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-10-29T17:24:25+00:00" }, { "name": "thecodingmachine/safe", @@ -912,27 +916,27 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e" + "reference": "b598aa890815b8df16363271b659d73280129101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", - "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.2", - "squizlabs/php_codesniffer": "^3.13.4 || ^4.0" + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevcs": "^1.2.0", "phpcsstandards/phpcsdevtools": "^1.2.1", "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, @@ -990,32 +994,32 @@ "type": "thanks_dev" } ], - "time": "2025-10-28T17:00:02+00:00" + "time": "2025-11-12T23:06:57+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.1.3", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9" + "reference": "fa82d14ad1c1713224a52c66c78478145fe454ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", - "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/fa82d14ad1c1713224a52c66c78478145fe454ba", + "reference": "fa82d14ad1c1713224a52c66c78478145fe454ba", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevcs": "^1.2.0", "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", @@ -1083,7 +1087,7 @@ "type": "thanks_dev" } ], - "time": "2025-10-16T16:39:32+00:00" + "time": "2025-11-11T00:17:56+00:00" }, { "name": "psr/container", @@ -1140,16 +1144,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.4", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", - "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -1166,11 +1170,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -1220,7 +1219,7 @@ "type": "thanks_dev" } ], - "time": "2025-09-05T05:47:09+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "symfony/console", @@ -1952,16 +1951,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -2015,7 +2014,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -2026,12 +2025,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", diff --git a/includes/lib/Symfony/Component/CssSelector/Parser/Token.php b/includes/lib/Symfony/Component/CssSelector/Parser/Token.php index 2855d4e..a36b68c 100644 --- a/includes/lib/Symfony/Component/CssSelector/Parser/Token.php +++ b/includes/lib/Symfony/Component/CssSelector/Parser/Token.php @@ -31,6 +31,9 @@ class Token public const TYPE_NUMBER = 'number'; public const TYPE_STRING = 'string'; + /** + * @param self::TYPE_*|null $type + */ public function __construct( private ?string $type, private ?string $value, @@ -38,7 +41,10 @@ public function __construct( ) { } - public function getType(): ?int + /** + * @return self::TYPE_*|null + */ + public function getType(): ?string { return $this->type; } From 564734f3761d708e48fe91ef3ef2010657d3fb0e Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Sat, 15 Nov 2025 16:34:32 +0000 Subject: [PATCH 08/11] Remove forced Cache-Control header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to prove this won’t affect full page caching before adding it --- includes/classes/Security/Headers.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 265f82f..212994f 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -44,9 +44,6 @@ public function set_security_headers( $headers ) { // Prevent clickjacking inside iframes (legacy) 'X-Frame-Options' => 'SAMEORIGIN', - // Good default for non-sensitive resources - 'Cache-Control' => 'no-cache', - // Permissive CSP (websites should customise this, ideally) 'Content-Security-Policy' => "default-src 'self' * 'unsafe-inline' 'unsafe-eval' data: blob:", ]; From 0a91b236173e7d58ef4d9ab6c315d7478750c38b Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Sat, 15 Nov 2025 17:21:23 +0000 Subject: [PATCH 09/11] Correction to using the wp_headers filter --- includes/classes/Security/Headers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 212994f..154fad6 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -10,7 +10,7 @@ use Eighteen73\Orbit\Singleton; /** - * Completely disable XML-RPC + * Add a sensible baseline set of security headers to the response. */ class Headers { use Singleton; @@ -19,7 +19,7 @@ class Headers { * Setup module */ public function setup() { - add_action( 'wp_headers', [ $this, 'set_security_headers' ], 99, 1 ); + add_filter( 'wp_headers', [ $this, 'set_security_headers' ], 99, 1 ); } /** From 95619672189d6d6ba158693491d7ad20848c4bdc Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Sat, 15 Nov 2025 17:21:37 +0000 Subject: [PATCH 10/11] Improve the default CSP --- includes/classes/Security/Headers.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 154fad6..0cd1155 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -45,7 +45,13 @@ public function set_security_headers( $headers ) { 'X-Frame-Options' => 'SAMEORIGIN', // Permissive CSP (websites should customise this, ideally) - 'Content-Security-Policy' => "default-src 'self' * 'unsafe-inline' 'unsafe-eval' data: blob:", + 'Content-Security-Policy' => implode('; ', [ + "default-src 'self' https:", + "img-src 'self' https: data: blob:", + "script-src 'self' https: 'unsafe-inline'", + "style-src 'self' https: 'unsafe-inline'", + "frame-ancestors 'self'", + ]), ]; // Only if SSL From 8ef10a79dc253e85f4e6171f2faf643fe4206025 Mon Sep 17 00:00:00 2001 From: Ed Jeavons Date: Sat, 15 Nov 2025 20:51:05 +0000 Subject: [PATCH 11/11] Add CSP rule necessary for functionality --- includes/classes/Security/Headers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/classes/Security/Headers.php b/includes/classes/Security/Headers.php index 0cd1155..0ce3940 100644 --- a/includes/classes/Security/Headers.php +++ b/includes/classes/Security/Headers.php @@ -48,7 +48,7 @@ public function set_security_headers( $headers ) { 'Content-Security-Policy' => implode('; ', [ "default-src 'self' https:", "img-src 'self' https: data: blob:", - "script-src 'self' https: 'unsafe-inline'", + "script-src 'self' https: 'unsafe-inline' 'unsafe-eval'", "style-src 'self' https: 'unsafe-inline'", "frame-ancestors 'self'", ]),