[TikiWiki-commits] [Git][tikiwiki/tiki][29.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 | <6a10b6bbb1eb5_3819278881417@gitlab-sidekiq-low-urgency-cpu-bound-v2-5755d7f9f9-r98p8.mail> |
Elifeleti Mukisa Dan pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki Commits: 1b677d74 by Elifeleti Mukisa Dan at 2026-05-22T19:57:17+00:00 [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 - - - - - 6 changed files: - _htaccess - + installer/schema/20260416_delete_legacy_serialized_newsletter_infos_tiki.php - lib/core/Tiki/Smarty/SmartyTiki.php - lib/prefs/http.php - templates/admin/include_security.tpl - tiki-send_newsletters.php Changes: ===================================== _htaccess ===================================== @@ -12,10 +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: +# 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)$"> @@ -75,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/core/Tiki/Smarty/SmartyTiki.php ===================================== @@ -310,6 +310,18 @@ class SmartyTiki 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); @@ -321,19 +333,108 @@ class SmartyTiki 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 ===================================== @@ -73,6 +73,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.'), @@ -114,8 +235,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' => [ @@ -126,7 +248,7 @@ function prefs_http_list() 'name' => tra('HTTP header public-key-pins'), 'description' => tra('The public-key-pins header associates a specific cryptographic public key with a certain web server to decrease the risk of MITM attacks with forged certificates. If one or several keys are pinned and none of them are used by the server, the browser will not accept the response as legitimate, and will not display it.'), 'type' => 'flag', - 'default' => 'y', + 'default' => 'n', 'perspective' => false, 'tags' => ['advanced'], ], ===================================== templates/admin/include_security.tpl ===================================== @@ -162,6 +162,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 ===================================== @@ -245,7 +245,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) { @@ -294,7 +301,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/1b677d740b05b9bebb14798c7c956b0d1c8c059c -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1b677d740b05b9bebb14798c7c956b0d1c8c059c 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