[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] Add missing security headers and enable previously disabled headers with safe default values

"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a10a64fe1805_381926c07404b@gitlab-sidekiq-low-urgency-cpu-bound-v2-5755d7f9f9-xhhj2.mail>

Elifeleti Mukisa Dan pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
ba88f15b by Elifeleti Mukisa Dan at 2026-05-22T18:35:37+00:00
[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

- - - - -


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
=====================================
@@ -16,7 +16,10 @@
 # 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 .
 # If you change any rule in this file, also review and update ./web_config for IIS (Windows server).
-# See http://dev.tiki.org/Operating+System+independence#Keep_web.config_and_.htaccess_synchronized
+# 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)$">
@@ -76,6 +79,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
=====================================
@@ -340,6 +340,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);
@@ -381,8 +393,9 @@ class SmartyTiki extends Smarty
                         header('Cross-Origin-Embedder-Policy: credentialless');
                         break;
                     case 'unsafe-none':
+                        header('Cross-Origin-Embedder-Policy: unsafe-none');
+                        break;
                     default:
-                        header_remove('Cross-Origin-Embedder-Policy');
                         break;
                 }
             }
@@ -413,24 +426,45 @@ class SmartyTiki extends Smarty
                         header('Cross-Origin-Opener-Policy: same-origin-plus-coep');
                         break;
                     case 'unsafe-none':
+                        header('Cross-Origin-Opener-Policy: unsafe-none');
+                        break;
                     default:
-                        header_remove('Cross-Origin-Opener-Policy');
                         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
=====================================
@@ -98,7 +98,7 @@ function prefs_http_list()
                 'require-corp' => tra('Require-CORP'),
                 'credentialless' => tra('Credentialless'),
             ],
-            'default' => '',
+            'default' => 'unsafe-none',
             'perspective' => false,
             'tags' => ['advanced'],
         ],
@@ -119,7 +119,7 @@ function prefs_http_list()
                 'same-site' => tra('Same-Site: Only requests from the same site are allowed.'),
                 'cross-origin' => tra('Cross-Origin: Allows requests from any origin.'),
             ],
-            'default' => '',
+            'default' => 'same-site',
             'perspective' => false,
             'tags' => ['advanced'],
             'help' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy',
@@ -142,11 +142,66 @@ function prefs_http_list()
                 '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' => '',
+            '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.'),
@@ -234,8 +289,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' => [
@@ -246,7 +302,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
=====================================
@@ -189,6 +189,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>
             <fieldset>
                 <legend class="h3">{tr}.htaccess Security{/tr}{help url="Security"}</legend>


=====================================
tiki-send_newsletters.php
=====================================
@@ -251,7 +251,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) {
@@ -300,7 +307,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/ba88f15b54d0b3be0f09d2ede90b1bfa3d3c16b3

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/ba88f15b54d0b3be0f09d2ede90b1bfa3d3c16b3
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.