[TikiWiki-commits] [Git][tikiwiki/tiki][24.x] [BP][FIX] Add missing security headers and enable previously disabled headers...
"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a119b5051baa_38809a6fc80298@gitlab-sidekiq-low-urgency-cpu-bound-v2-d7f87744c-j4kb2.mail> |
Elifeleti Mukisa Dan pushed to branch 24.x at Tiki Wiki CMS Groupware / Tiki Commits: 3fb92868 by Elifeleti Mukisa Dan at 2026-05-23T12:14:03+00:00 [BP][FIX] Add missing security headers and enable previously disabled headers... --- * [BP][FIX] Add missing security headers and enable previously disabled headers... --- * [BP][FIX] Add missing security headers and enable previously disabled headers... --- * [BP][FIX] Add missing security headers and enable previously disabled headers with safe default values --- * [FIX] Add missing security headers and enable previously disabled headers with safe default values --- * [FIX] Add missing security headers and enable previously disabled headers with safe default values (cherry picked from commit 91ee7e6f7e41dccf526f7a8c8fdbced820cec5cc) f163323b [FIX] tiki-send_newsletters.php: prevent insecure deserialization of... ed7dc58b [FIX] Add missing security headers and enable previously disabled headers with safe default values 54039433 [FIX] Update default values for HTTP security headers to 'n' for enhanced security ba45a9ba [FIX] Update default values for HTTP security headers to 'n' for enhanced security Co-authored-by: Alfred Syatsukwa <[email protected]> See merge request tikiwiki/tiki!10293 (cherry picked from commit ba88f15b54d0b3be0f09d2ede90b1bfa3d3c16b3) a6b04fb2 [FIX] Add missing security headers and enable previously disabled headers with safe default values Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!10312 See merge request tikiwiki/tiki!10324 See merge request tikiwiki/tiki!10339 See merge request tikiwiki/tiki!10351 - - - - - 6 changed files: - _htaccess - + installer/schema/20260416_delete_legacy_serialized_newsletter_infos_tiki.php - lib/init/smarty.php - lib/prefs/http.php - templates/admin/include_security.tpl - tiki-send_newsletters.php Changes: ===================================== _htaccess ===================================== @@ -12,8 +12,16 @@ # Option 4: Add the content of this file to your httpd.conf. # The last two options should be repeated when the reference _htaccess file changes (when upgrading Tiki). # -# DEVELOPERS: This configuration must be kept synchronized with the configuration for other Web servers. +# DEVELOPERS: +# This is the sample configuration file for Apache. Must be linked to .htaccess to be active. +# This configuration must be kept synchronized with the configuration for other Web servers. # See http://dev.tiki.org/Operating+System+independence#Keep_web.config_and_.htaccess_synchronized +# This configuration must be kept synchronized with the configuration for other Web servers . +# If you change any rule in this file, also review and update ./web_config for IIS (Windows server). +# nginx/Caddy/lighttpd: .htaccess is not read; mirror needed rules in the server config. Dynamic +# HTML responses already get security headers from Tiki (PHP). For static files only, use the +# same header names/values as in the FilesMatch block below (e.g. nginx: add_header ... always; +# inside a location ~* \.(css|js|...)$ block). See http://dev.tiki.org/Operating+System+independence#Keep_web.config_and_.htaccess_synchronized # -- Prevent Browsing of Certain File Extensions -- # <FilesMatch "\.(bak|inc|lib|sh|tpl|sql|shtml|asp|xml\.dist)$"> @@ -73,6 +81,24 @@ FileETag none # -- httpoxy mitigation -- # RequestHeader unset Proxy early + # -- Security Headers (static files only; Apache) -- # + # Tiki/PHP sends these for dynamic pages. Applying them globally duplicated headers on PHP + # responses. Non-Apache stacks: see DEVELOPERS note at top of this file; + # use the same names/values in nginx add_header inside a static location. + <FilesMatch "(?i)\.(css|js|mjs|map|png|jpe?g|gif|svgz?|ico|woff2?|ttf|eot|webp|json|txt|xml|mp3|mp4|webm|pdf)$"> + Header always setifempty X-Content-Type-Options "nosniff" + Header always setifempty X-Frame-Options "SAMEORIGIN" + Header always setifempty Referrer-Policy "strict-origin-when-cross-origin" + Header always setifempty X-Permitted-Cross-Domain-Policies "none" + Header always setifempty Cross-Origin-Opener-Policy "same-origin-allow-popups" + Header always setifempty Cross-Origin-Resource-Policy "same-site" + Header always setifempty Cross-Origin-Embedder-Policy "unsafe-none" + </FilesMatch> + # Uncomment and configure the following once you have audited your CSP needs: + # Header always setifempty Content-Security-Policy "default-src 'self'; ..." + # Uncomment only on full HTTPS; add includeSubDomains/preload only after subdomain audit: + # Header always setifempty Strict-Transport-Security "max-age=63072000" + # Option 2: #Header unset ETag ===================================== installer/schema/20260416_delete_legacy_serialized_newsletter_infos_tiki.php ===================================== @@ -0,0 +1,48 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +use Tiki\Installer\Installer; + +/** + * Delete legacy serialized .infos files for newsletter attachments. + * + * These files were previously written using serialize() and read back with unserialize(), + * which is a security risk (insecure deserialization). New files are written as JSON. + * This migration removes legacy serialized .infos files and any non-array JSON metadata from tmp, + * along with the sibling attachment file. Runtime code never calls unserialize() on these paths. + * + * @param Installer $installer + */ +function upgrade_20260416_delete_legacy_serialized_newsletter_infos_tiki($installer) +{ + global $prefs; + + $tmpDir = $prefs['tmpDir'] ?? 'temp'; + $pattern = $tmpDir . '/newsletterfile-*.infos'; + $files = glob($pattern); + + if (! is_array($files)) { + return; + } + + foreach ($files as $file) { + $content = @file_get_contents($file); + if ($content === false) { + continue; + } + + // Keep only JSON arrays (metadata shape). Legacy serialize(), invalid JSON, scalars → delete both files. + $decoded = json_decode($content, true); + if (! is_array($decoded)) { + @unlink($file); + // Also remove the associated attachment data file (same path without .infos) + $dataFile = substr($file, 0, -6); // strip '.infos' + if (file_exists($dataFile)) { + @unlink($dataFile); + } + } + } +} ===================================== lib/init/smarty.php ===================================== @@ -412,6 +412,18 @@ class Smarty_Tiki extends Smarty $public_key_pins = $prefs['http_header_public_key_pins']; } + if (! isset($prefs['http_header_referrer_policy'])) { + $referrer_policy = false; + } else { + $referrer_policy = $prefs['http_header_referrer_policy']; + } + + if (! isset($prefs['http_header_permitted_cross_domain_policies'])) { + $permitted_cross_domain_policies = false; + } else { + $permitted_cross_domain_policies = $prefs['http_header_permitted_cross_domain_policies']; + } + if ($frame == 'y') { $header_value = $prefs['http_header_frame_options_value']; header('X-Frame-Options: ' . $header_value); @@ -423,19 +435,108 @@ class Smarty_Tiki extends Smarty if ($content_type_options == 'y') { header('X-Content-Type-Options: nosniff'); } + if ($access_control_allow_credentials === 'y') { + header('Access-Control-Allow-Credentials: true'); + } + if ($access_control_allow_methods === 'y') { + $header_value = trim($prefs['http_header_access_control_allow_methods_value']); + if ($access_control_allow_credentials === 'y' && $header_value === '*') { + Feedback::error(tr("CORS configuration error: Wildcard (*) is not allowed for Access-Control-Allow-Methods when http_header_access_control_allow_credential is enabled")); + header_remove('Access-Control-Allow-Methods'); + } else { + header('Access-Control-Allow-Methods: ' . $header_value); + } + } + if ($access_control_allow_headers === 'y') { + $headers_value = trim($prefs['http_header_access_control_allow_headers_value']); + if ($access_control_allow_credentials === 'y' && $headers_value === '*') { + Feedback::error(tr("CORS configuration error: Wildcard (*) is not allowed for Access-Control-Allow-Headers when Access-Control-Allow-Credentials is enabled.")); + header_remove('Access-Control-Allow-Headers'); + } else { + header('Access-Control-Allow-Headers: ' . $headers_value); + } + } + if ($cross_origin_embedder_policy === 'y') { + switch (trim($prefs['http_header_cross_origin_embedder_policy_value'])) { + case 'require-corp': + header('Cross-Origin-Embedder-Policy: require-corp'); + break; + case 'credentialless': + header('Cross-Origin-Embedder-Policy: credentialless'); + break; + case 'unsafe-none': + header('Cross-Origin-Embedder-Policy: unsafe-none'); + break; + default: + break; + } + } + if ($cross_origin_resource_policy === 'y') { + switch (trim($prefs['http_header_cross_origin_resource_policy_value'])) { + case 'same-origin': + header('Cross-Origin-Resource-Policy: same-origin'); + break; + case 'same-site': + header('Cross-Origin-Resource-Policy: same-site'); + break; + case 'cross-origin': + header('Cross-Origin-Resource-Policy: cross-origin'); + break; + default: + break; + } + } + if ($cross_origin_opener_policy === 'y') { + switch (trim($prefs['http_header_cross_origin_opener_policy_value'])) { + case 'same-origin': + header('Cross-Origin-Opener-Policy: same-origin'); + break; + case 'same-origin-allow-popups': + header('Cross-Origin-Opener-Policy: same-origin-allow-popups'); + break; + case 'same-origin-plus-coep': + header('Cross-Origin-Opener-Policy: same-origin-plus-coep'); + break; + case 'unsafe-none': + header('Cross-Origin-Opener-Policy: unsafe-none'); + break; + default: + break; + } + } if ($content_security_policy == 'y') { - $header_value = $prefs['http_header_content_security_policy_value']; - header('Content-Security-Policy: ' . $header_value); + $header_value = trim($prefs['http_header_content_security_policy_value']); + if ($header_value !== '') { + header('Content-Security-Policy: ' . $header_value); + } } if ($strict_transport_security == 'y') { - $header_value = $prefs['http_header_strict_transport_security_value']; - header('Strict-Transport-Security: ' . $header_value); + $header_value = trim($prefs['http_header_strict_transport_security_value']); + if ($header_value !== '') { + header('Strict-Transport-Security: ' . $header_value); + } } if ($public_key_pins == 'y') { - $header_value = $prefs['http_header_public_key_pins_value']; - header('Public-Key-Pins: ' . $header_value); + $header_value = trim($prefs['http_header_public_key_pins_value']); + if ($header_value !== '') { + header('Public-Key-Pins: ' . $header_value); + } + } + + if ($referrer_policy === 'y') { + $header_value = trim($prefs['http_header_referrer_policy_value']); + if ($header_value !== '') { + header('Referrer-Policy: ' . $header_value); + } + } + + if ($permitted_cross_domain_policies === 'y') { + $header_value = trim($prefs['http_header_permitted_cross_domain_policies_value']); + if ($header_value !== '') { + header('X-Permitted-Cross-Domain-Policies: ' . $header_value); + } } } ===================================== lib/prefs/http.php ===================================== @@ -75,6 +75,127 @@ function prefs_http_list() 'http_header_xss_protection', ], ], + 'http_header_cross_origin_embedder_policy' => [ + 'name' => tra('HTTP header cross-origin-embedder-policy'), + 'description' => tra('Controls the loading of cross-origin resources in a document. Setting this header helps enhance security by ensuring that loaded resources explicitly grant permission to be loaded.'), + 'type' => 'flag', + 'default' => 'n', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_cross_origin_embedder_policy_value' => [ + 'name' => tra('Header value'), + 'description' => tra('Specifies the policy for loading cross-origin resources. "Require-CORP" requires cross-origin resources to have CORP headers. "Credentialless" allows loading cross-origin resources without credentials. "Unsafe-none" applies no restrictions.'), + 'type' => 'list', + 'options' => [ + 'unsafe-none' => tra('None'), + 'require-corp' => tra('Require-CORP'), + 'credentialless' => tra('Credentialless'), + ], + 'default' => 'unsafe-none', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_cross_origin_resource_policy' => [ + 'name' => tra('HTTP header Cross-Origin-Resource-Policy'), + 'description' => tra('Defines which cross-origin requests are allowed to access resources on your site. This header can help prevent other sites from reading or loading your site\'s resources without permission.'), + 'type' => 'flag', + 'default' => 'n', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_cross_origin_resource_policy_value' => [ + 'name' => tra('Header value'), + 'description' => tra('Determines which origins are allowed to access resources. "Same-Origin" only allows your own site to access resources. "Same-Site" extends this to your entire site, including subdomains. "Cross-Origin" allows any site to access the resources.'), + 'type' => 'list', + 'options' => [ + 'same-origin' => tra('Same-Origin: Only same-origin requests are allowed.'), + 'same-site' => tra('Same-Site: Only requests from the same site are allowed.'), + 'cross-origin' => tra('Cross-Origin: Allows requests from any origin.'), + ], + 'default' => 'same-site', + 'perspective' => false, + 'tags' => ['advanced'], + 'help' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy', + ], + 'http_header_cross_origin_opener_policy' => [ + 'name' => tra('HTTP header Cross-Origin-Opener-Policy'), + 'description' => tra('Enables or disables the sending of the Cross-Origin-Opener-Policy header in HTTP responses from your site. This header controls how the document may interact with other browsing contexts.'), + 'type' => 'flag', + 'default' => 'n', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_cross_origin_opener_policy_value' => [ + 'name' => tra('Header value'), + 'description' => tra('Specifies the policy for cross-origin opener policy header.'), + 'type' => 'list', + 'options' => [ + 'same-origin' => tra('Same-Origin: Allows the document to be opened only by pages from the same origin.'), + 'same-origin-allow-popups' => tra('Same-Origin-Allow-Popups: Allows the document to be opened by pages from the same origin, and allows those pages to open popups.'), + 'same-origin-plus-coep' => tra('Same-Origin-Plus-COEP: Allows the document to be opened only by pages from the same origin, and sets the Cross-Origin-Embedder-Policy header to `require-corp`.'), + 'unsafe-none' => tra('None: No specific policy is set.'), + ], + 'default' => 'same-origin-allow-popups', + 'perspective' => false, + 'tags' => ['advanced'], + 'help' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy', + ], + 'http_header_referrer_policy' => [ + 'name' => tra('HTTP header Referrer-Policy'), + 'description' => tra('The Referrer-Policy HTTP header controls how much referrer information (sent via the Referer header) should be included with requests.'), + 'type' => 'flag', + 'default' => 'y', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_referrer_policy_value' => [ + 'name' => tra('Header value'), + 'description' => tra('Specifies the referrer policy. "strict-origin-when-cross-origin" is the recommended safe default.'), + 'type' => 'list', + 'options' => [ + 'no-referrer' => tra('no-referrer: Never send the Referer header.'), + 'no-referrer-when-downgrade' => tra('no-referrer-when-downgrade: Send full URL for same-origin, omit on downgrade.'), + 'origin' => tra('origin: Send only the origin.'), + 'origin-when-cross-origin' => tra('origin-when-cross-origin: Full URL for same-origin, origin only for cross-origin.'), + 'same-origin' => tra('same-origin: Send referrer only to same-origin requests.'), + 'strict-origin' => tra('strict-origin: Send origin only when protocol security is maintained.'), + 'strict-origin-when-cross-origin' => tra('strict-origin-when-cross-origin: Full URL same-origin, origin cross-origin, nothing on downgrade. (Recommended)'), + 'unsafe-url' => tra('unsafe-url: Always send full URL (not recommended).'), + ], + 'default' => 'strict-origin-when-cross-origin', + 'perspective' => false, + 'tags' => ['advanced'], + 'dependencies' => [ + 'http_header_referrer_policy', + ], + 'help' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy', + ], + 'http_header_permitted_cross_domain_policies' => [ + 'name' => tra('HTTP header X-Permitted-Cross-Domain-Policies'), + 'description' => tra('The X-Permitted-Cross-Domain-Policies header controls how Adobe products (Flash, Acrobat) and Silverlight may access cross-domain data.'), + 'type' => 'flag', + 'default' => 'y', + 'perspective' => false, + 'tags' => ['advanced'], + ], + 'http_header_permitted_cross_domain_policies_value' => [ + 'name' => tra('Header value'), + 'type' => 'list', + 'options' => [ + 'none' => tra('none: No cross-domain policies allowed. (Recommended)'), + 'master-only' => tra('master-only: Only the master policy file is allowed.'), + 'by-content-type' => tra('by-content-type: Only policy files served with Content-Type: text/x-cross-domain-policy are allowed.'), + 'all' => tra('all: All policy files are allowed.'), + ], + 'default' => 'none', + 'perspective' => false, + 'tags' => ['advanced'], + 'dependencies' => [ + 'http_header_permitted_cross_domain_policies', + ], + 'help' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Permitted-Cross-Domain-Policies', + ], 'http_header_content_type_options' => [ 'name' => tra('HTTP header x-content-type-options'), 'description' => tra('The x-content-type-options header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed.'), @@ -111,8 +232,9 @@ function prefs_http_list() ], 'http_header_strict_transport_security_value' => [ 'name' => tra('Header value'), + 'description' => tra('Add includeSubDomains only if every subdomain is served over HTTPS. Add preload only after careful review at hstspreload.org.'), 'type' => 'text', - 'default' => '', + 'default' => 'max-age=63072000', 'perspective' => false, 'tags' => ['advanced'], 'dependencies' => [ ===================================== templates/admin/include_security.tpl ===================================== @@ -166,6 +166,16 @@ <div class="adminoptionboxchild" id="http_header_public_key_pins_childcontainer"> {preference name=http_header_public_key_pins_value} </div> + + {preference name=http_header_referrer_policy} + <div class="adminoptionboxchild" id="http_header_referrer_policy_childcontainer"> + {preference name=http_header_referrer_policy_value} + </div> + + {preference name=http_header_permitted_cross_domain_policies} + <div class="adminoptionboxchild" id="http_header_permitted_cross_domain_policies_childcontainer"> + {preference name=http_header_permitted_cross_domain_policies_value} + </div> </fieldset> {/tab} ===================================== tiki-send_newsletters.php ===================================== @@ -177,7 +177,8 @@ if (isset($_REQUEST['is_html'])) { $parserlib = TikiLib::lib('parser'); $templateslib = TikiLib::lib('template'); -if (! empty($_REQUEST['templateId']) && +if ( + ! empty($_REQUEST['templateId']) && ! isset($_REQUEST['preview']) && ! isset($_REQUEST['save_only']) && ! isset($_REQUEST['send']) && @@ -205,7 +206,14 @@ if (isset($_REQUEST['newsletterfile'])) { $f = []; if ((strlen($id) == 32) && preg_match('/^[0-9a-f]{32}$/', $id)) { // this is a valid md5 hash, so the file was just saved at preview time $fpath = $prefs['tmpDir'] . '/newsletterfile-' . $id; - $f = unserialize(file_get_contents($fpath . '.infos')); + $infosContent = file_get_contents($fpath . '.infos'); + if ($infosContent === false) { + continue; + } + $f = json_decode($infosContent, true); + if (! is_array($f)) { + continue; + } $f['path'] = $fpath; $newsletterfiles[] = $f; } elseif ((int)$_REQUEST['nlId'] > 0) { @@ -253,7 +261,7 @@ foreach ($info['files'] as $k => $newsletterfile) { $info['files'][$k]['path'] = $tmpfname; $info['files'][$k]['id'] = $tmpfnamekey; $info['files'][$k]['filename'] = $tmpfnamekey; - file_put_contents($tmpfname . '.infos', serialize($info['files'][$k])); + file_put_contents($tmpfname . '.infos', json_encode($info['files'][$k])); } } } View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/3fb928680528c0c9db9af5757823ed67607d14d9 -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/3fb928680528c0c9db9af5757823ed67607d14d9 You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help _______________________________________________ TikiWiki-cvs mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs