From 04e5ce22de1a40f405d1831d2e5a8c9a7d8c5489 Mon Sep 17 00:00:00 2001 From: Nicolas Varlot Date: Tue, 12 May 2026 23:08:34 +0200 Subject: [PATCH 1/5] fix: split large Content-Security-Policy headers over multiple HTTP headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the Content-Security-Policy header exceeds 7800 bytes (which happens with federation across ~20+ trusted servers), split it into multiple header() calls. Apache mod_proxy_fcgi otherwise fails with AH01070 because HUGE_STRING_LEN is hardcoded at 8192 bytes. Per CSP Level 3 ยง3.1, multiple CSP headers are enforced as the intersection of policies, so client behavior is unchanged. Signed-off-by: Nicolas Varlot --- lib/private/AppFramework/Http/Output.php | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index aee009b4654d1..d55bb8edf080f 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -46,7 +46,32 @@ public function setReadfile($path) { */ #[\Override] public function setHeader($header) { - header($header); + $maxLen = 7800; + if (strlen($header) > $maxLen) { + foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { + if (strncmp($header, $prefix, strlen($prefix)) === 0) { + $value = ltrim(substr($header, strlen($prefix))); + $directives = array_filter(array_map('trim', explode(';', $value))); + $segment = ''; + $first = true; + foreach ($directives as $directive) { + $candidate = $segment === '' ? $directive : $segment . ';' . $directive; + if (strlen($prefix . ' ' . $candidate . ';') > $maxLen && $segment !== '') { + header($prefix . ' ' . $segment . ';', $first); + $first = false; + $segment = $directive; + } else { + $segment = $candidate; + } + } + if ($segment !== '') { + header($prefix . ' ' . $segment . ';', $first); + } + return; + } + } + } + header($header); } /** From d31ffcc1e93e5ba2d40e33252f39551371eb5ab3 Mon Sep 17 00:00:00 2001 From: Nicolas Varlot Date: Tue, 12 May 2026 23:24:33 +0200 Subject: [PATCH 2/5] fix indents Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Nicolas Varlot --- lib/private/AppFramework/Http/Output.php | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index d55bb8edf080f..71e1e3a7394b3 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -46,32 +46,32 @@ public function setReadfile($path) { */ #[\Override] public function setHeader($header) { - $maxLen = 7800; - if (strlen($header) > $maxLen) { - foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { - if (strncmp($header, $prefix, strlen($prefix)) === 0) { - $value = ltrim(substr($header, strlen($prefix))); - $directives = array_filter(array_map('trim', explode(';', $value))); - $segment = ''; - $first = true; - foreach ($directives as $directive) { - $candidate = $segment === '' ? $directive : $segment . ';' . $directive; - if (strlen($prefix . ' ' . $candidate . ';') > $maxLen && $segment !== '') { - header($prefix . ' ' . $segment . ';', $first); - $first = false; - $segment = $directive; - } else { - $segment = $candidate; - } - } - if ($segment !== '') { - header($prefix . ' ' . $segment . ';', $first); - } - return; - } - } - } - header($header); + $maxLen = 7800; + if (strlen($header) > $maxLen) { + foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { + if (strncmp($header, $prefix, strlen($prefix)) === 0) { + $value = ltrim(substr($header, strlen($prefix))); + $directives = array_filter(array_map('trim', explode(';', $value))); + $segment = ''; + $first = true; + foreach ($directives as $directive) { + $candidate = $segment === '' ? $directive : $segment . ';' . $directive; + if (strlen($prefix . ' ' . $candidate . ';') > $maxLen && $segment !== '') { + header($prefix . ' ' . $segment . ';', $first); + $first = false; + $segment = $directive; + } else { + $segment = $candidate; + } + } + if ($segment !== '') { + header($prefix . ' ' . $segment . ';', $first); + } + return; + } + } + } + header($header); } /** From b3101fe27d54c84a0afcc7c52c4e454918c19825 Mon Sep 17 00:00:00 2001 From: Nicolas Varlot Date: Tue, 12 May 2026 23:43:50 +0200 Subject: [PATCH 3/5] Add comments for hardcoded 7800 Added comments to explain the hardcoded value Signed-off-by: Nicolas Varlot --- lib/private/AppFramework/Http/Output.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index 71e1e3a7394b3..4394d22634110 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -46,6 +46,9 @@ public function setReadfile($path) { */ #[\Override] public function setHeader($header) { + // Apache mod_proxy_fcgi parses FCGI response headers with a buffer + // hardcoded at HUGE_STRING_LEN (8192 bytes in httpd.h); 7800 leaves + // a safety margin for the status line and surrounding header bytes. $maxLen = 7800; if (strlen($header) > $maxLen) { foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { From 5f667584a7f4717ce29914cf44730c15667fa1ff Mon Sep 17 00:00:00 2001 From: Nicolas Varlot Date: Wed, 13 May 2026 09:13:18 +0200 Subject: [PATCH 4/5] Update lib/private/AppFramework/Http/Output.php Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: Nicolas Varlot --- lib/private/AppFramework/Http/Output.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index 4394d22634110..0f0b3a413f8c8 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -52,7 +52,7 @@ public function setHeader($header) { $maxLen = 7800; if (strlen($header) > $maxLen) { foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { - if (strncmp($header, $prefix, strlen($prefix)) === 0) { + if (str_starts_with($header, $prefix)) { $value = ltrim(substr($header, strlen($prefix))); $directives = array_filter(array_map('trim', explode(';', $value))); $segment = ''; From 95470a8a7015cabd51635f762caaf68faaae8ee7 Mon Sep 17 00:00:00 2001 From: Nicolas Varlot Date: Wed, 13 May 2026 09:13:53 +0200 Subject: [PATCH 5/5] Update lib/private/AppFramework/Http/Output.php Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: Nicolas Varlot --- lib/private/AppFramework/Http/Output.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index 0f0b3a413f8c8..19ec214e283ee 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -54,7 +54,7 @@ public function setHeader($header) { foreach (['Content-Security-Policy:', 'Feature-Policy:'] as $prefix) { if (str_starts_with($header, $prefix)) { $value = ltrim(substr($header, strlen($prefix))); - $directives = array_filter(array_map('trim', explode(';', $value))); + $directives = array_filter(array_map(trim(...), explode(';', $value))); $segment = ''; $first = true; foreach ($directives as $directive) {