Skip to content

fix: harden PHP request parsing (post_max_size, max_input_nesting_level) - #586

Merged
oc-tmueller merged 3 commits into
masterfrom
fix/php-input-hardening
Oct 27, 2026
Merged

oc-tmueller merged 3 commits into
masterfrom
fix/php-input-hardening

Conversation

@oc-tmueller

@oc-tmueller oc-tmueller commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Post-patch hardening for CVE-2026-109618 / CVE-2026-109619, following Freexian's advice. The fixed PHP introduces a hard memory limit that kills the worker when it is reached, and post_max_size should stay below a quarter of memory_limit. Until now post_max_size followed OWNCLOUD_MAX_UPLOAD (20G); on ownCloud 10, .htaccess set it to 513M. Both are above the 512M memory_limit.

Changes, for both v22.04 and v24.04:

  • New OWNCLOUD_POST_MAX_SIZE, default 100M. It sets post_max_size in the ini and is also enforced in the vhost with php_admin_value, so ownCloud's .htaccess php_value post_max_size 513M can't raise it. upload_max_filesize stays on OWNCLOUD_MAX_UPLOAD.
  • max_input_nesting_level 64 → 6.
  • session.upload_progress.enabled = Off. GHSA-6wmf-4rv5-p3rp names it as the mod_php attack path for CVE-2026-109619 and recommends keeping it off after patching. Nothing in core or its bundled apps uses it (searched on 10.16.6 and 11.0.1). It is verified off at runtime under mod_php on both lines.
  • expose_php = Off is now set explicitly. Ubuntu's php.ini already had it off, so this changes no behaviour.
  • ENVIRONMENT.md and CHANGELOG.

Verified on local builds of both bases with the 10.16.6 and 11.0.1 server images on top:

  • Runtime post_max_size is 100M on both. On 10.16, .htaccess no longer wins: the published image reports 513M, and the published 11.0.1 reports 20G.
  • OWNCLOUD_POST_MAX_SIZE=200M gives 200M. max_input_nesting_level is 6 (published: 64). No X-Powered-By header.
  • A 200 MB WebDAV PUT returns 201 with the full size. A form POST to the OCS share API succeeds.
  • A 150 MB multipart POST is no longer parsed: one startup warning, no memory exhaustion. A 50 MB one still parses.
  • A raw POST body above the limit is still delivered whole through php://input (tested on PHP 7.4 and 8.3), so WOPI-style saves keep working.

Known side effects:

  • The Files web UI tooltip now reads "Upload (max. 100 MB)", because core shows min(upload_max_filesize, post_max_size). Uploads themselves are chunked WebDAV PUT and not limited by it.
  • Form or multipart POSTs above 100M are no longer parsed.
  • v20.04, which CI does not build, is left unchanged.

🤖 Generated with Claude Code

oc-tmueller and others added 2 commits October 22, 2026 16:39
post_max_size no longer follows OWNCLOUD_MAX_UPLOAD (20G). It defaults
to 100M, below a quarter of the 512M memory_limit as Freexian recommends
after CVE-2026-109618/-109619, is configurable with the new
OWNCLOUD_POST_MAX_SIZE, and is enforced with php_admin_value so that
ownCloud's .htaccess cannot raise it. max_input_nesting_level drops to
6, and expose_php = Off is made explicit. File uploads use WebDAV PUT
and are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
A value with a space no longer breaks Apache's config parsing. On
ownCloud 10, .htaccess pins memory_limit to 512M and upload_max_filesize
to 513M, so the environment variables do not set those there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
@oc-tmueller
oc-tmueller marked this pull request as draft October 22, 2026 14:53
GHSA-6wmf-4rv5-p3rp names session upload progress as the confirmed
mod_php attack path for CVE-2026-109619 and recommends keeping it off
after patching. Nothing in ownCloud core or its bundled apps uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
@oc-tmueller

Copy link
Copy Markdown
Member Author

Compatibility with ownCloud 10.16.6 and 11.0.1

All checks below ran on images built from 2eef8ca, the head of this PR.

Acceptance tests

The full API and CLI acceptance suites passed on both lines with no regressions. Every scenario that came out differently from the published images is @local_storage locking, and it flips in both directions. owncloud-docker/server#738 runs the same suites in CI, and they are green for both lines.

Uploads and form parsing

Settings applied on both lines: post_max_size=100M (on 10.16 .htaccess can no longer raise it), max_input_nesting_level=6, session.upload_progress off.

10.16.6 11.0.1
WebDAV PUT of 150 MB 201, full size 201, full size
Chunked upload (v2), 3 × 50 MB assembled into 150 MB 201, full size 201, full size
application/octet-stream POST of 150 MB whole body in php://input whole body in php://input
Multipart POST of 50 MB parsed parsed
Multipart POST of 150 MB not parsed (intended); raw body still in php://input same
Form fields nested 5–6 levels deep kept kept
Form fields nested 7 levels deep dropped, other fields unaffected same

The 10.16 web UI uploads with WebDAV PUT (apps/files/js/file-upload.js), not multipart, so post_max_size doesn't limit it. Core's uploadLimit() now returns 100 MB on both lines. It only feeds the "Upload (max. 100 MB)" tooltip; nothing enforces it.

WOPI (Collabora / richdocuments)

Tested end to end with Collabora CODE 26.04.5.2: richdocuments 4.3.1 on 11.0.1, and 4.2.4 on 10.16.6.

  • A real editing session (open, edit, save) works on both lines. CODE saves with POST …/wopi/files/<id>/contents, Content-Type: application/octet-stream, X-WOPI-Override: PUT. richdocuments reads that body from php://input, which post_max_size doesn't limit.
  • PutFile with 150 MB and a real access token returns 200 on both lines, and the SHA-256 of the stored file matches what was sent.
  • PutRelativeFile (Save As) with 150 MB returns 200 on both lines, and the new file's SHA-256 matches.

WOPI (Microsoft Office Online)

Checked in the source of the wopi app (current release line and the ownCloud 10 line); not run end to end, because that needs Microsoft's service. PutFile and PutRelativeFile both read the raw body from php://input, and the access token comes from the query string. Neither depends on $_POST or $_FILES. As the table shows, a body over the limit stays in php://input whatever its Content-Type, so these saves keep working above 100 MB.

Side effects

  • Each WOPI save over 100 MB logs one Apache error-log warning: PHP Warning: POST Content-Length of … bytes exceeds the limit of 104857600 bytes. Nothing appears in owncloud.log.
  • Unrelated to this change: the wopi app's Save As reads the whole body into memory, so a Save As close to memory_limit (512M) can run out of memory. That limit was there before this PR.

🤖 Generated with Claude Code

@oc-tmueller
oc-tmueller marked this pull request as ready for review October 27, 2026 09:51

@kw-fscheuer kw-fscheuer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one gap; the approach itself is right. The four settings are the ones GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp name, php_admin_value is the correct way to beat core's .htaccess on 10.16, and the fixed PHP is already pinned on both lines (#584 for 22.04, #582 for 24.04), so this is post-patch hardening as described. Everything below is from reading the sources linked; nothing was run against a published image.

Blocking: multipart bodies without Content-Length bypass post_max_size on 22.04

Every size test in the description and in the compatibility comment sent a Content-Length. Without one, PHP 7.4 applies no body limit to multipart at all:

  • mod_php sets content_length = 0 when the header is absent (sapi_apache2.c#L516-L520). Apache removes Content-Length from the request whenever Transfer-Encoding is present (server/protocol.c#L1518-L1523), and an HTTP/2 request body can arrive without one.
  • The upstream PHP-7.4-CVE-2026-109618-109619 branch ("Minimal mitigation for EOL version") is the reference for the Freexian build; I have not seen Freexian's own patch, but the #584 test behaves like that branch. Its multipart reader has only the up-front content_length > post_max_size check (rfc1867.c#L728); fill_buffer just accumulates read_post_bytes (#L266). 8.3.36 added a running check in fill_buffer and at the end of the handler (rfc1867.c#L226, #L1417), and Ubuntu's noble source carries both, so 24.04 is covered inside PHP.
  • Consequence on 22.04: a single non-file part is erealloc'd until memory_limit, and on this branch every memory_limit hit is zend_mm_safe_error_ex → _exit(1) (zend_alloc.c#L370-L402). That is an unauthenticated worker kill per request, costing the attacker roughly memory_limit bytes of upload each time, with no error page, no owncloud.log entry and no shutdown handlers. It is also the first OOM strategy GHSA-w8hw-w9fx-j79w lists, which is why that advisory asks for the decoded request size to be enforced at the frontend.

LimitRequestBody 0 has to stay for WebDAV PUT and WOPI saves, so a plain cap is out. PHP only parses multipart/form-data, and browsers and the ownCloud clients never send it without a length, so denying that shape is safe on both lines (the fix on 22.04, defense in depth on 24.04). Next to the php_admin_value line:

# PHP 7.4 only bounds multipart bodies by Content-Length; a chunked or
# HTTP/2 body without one is parsed until memory_limit is hit.
<If "req('Content-Type') =~ m#^multipart/form-data#i && -z req('Content-Length')">
  Require all denied
</If>

Untested here, please verify in your rig. Apache enforces LimitRequestBody on chunked data as well (http_filters.c#L631-L645), so a method-scoped cap is the alternative, but it would have to sit well below memory_limit to protect a 7.4 worker and that collides with WOPI PutFile. Either way, please add a chunked case to the test matrix: resend the 150 MB multipart body with -H 'Transfer-Encoding: chunked' (curl switches to chunked when given that header) and confirm it is rejected on both lines.

Non-blocking

  1. OWNCLOUD_POST_MAX_SIZE is not validated. On 7.4 100MB parses as 100 bytes (only the last character is a multiplier), which breaks every form POST including login. On 8.3 it is 100 bytes plus an "Invalid quantity" warning on every request, raised through the vhost admin value before request startup, which is the diagnostic class GHSA-6wmf-4rv5-p3rp says to eliminate. 0 or a negative value means unlimited, because every check is post_max_size > 0. A ^[1-9][0-9]*[KkMmGg]?$ check in 10-base.sh, plus a warning when the value exceeds a quarter of the effective memory_limit (512M fixed on 10.16, as ENVIRONMENT.md already says), would close it.
  2. Part and file counts stay at defaults. max_multipart_body_parts exists on both lines (7.4 branch #L711) and defaults to max_input_vars + max_file_uploads, about 1020. Core's own multipart consumers are single-file forms (avatar, certificate upload), so an explicit few dozen parts and a handful of files is a safe bound. GHSA-w8hw-w9fx-j79w lists this among the post-patch controls.
  3. Document the 22.04 memory semantics. With the 7.4 mitigation a memory_limit hit is a process exit, not a fatal error: no error page, no owncloud.log line, shutdown functions and destructors skipped, one line on stderr. Your note about WOPI Save As near 512M now means a killed worker. On 24.04 the soft/hard split applies and the default memory_hard_limit_percent (200%) is fine. One CHANGELOG sentence would do.

Note, no change requested

The vhost php_admin_value is per-request INI application under mod_php, the consumer class in GHSA-6wmf-4rv5-p3rp. With the patch in place that is acceptable, and it cannot be avoided while core's .htaccess carries php_value lines and needs AllowOverride All. The long-term fix is on the core side: the block is already inert on 11.x (<IfModule mod_php8.c> never matches mod_php.c, which is why the published 11.0.1 reported 20G), so dropping it there and on 10.16 would let the vhost go back to startup-only config.

@oc-tmueller

Copy link
Copy Markdown
Member Author

@kw-fscheuer thanks for the thorough review. The chunked/HTTP/2 multipart gap on 22.04 and the three non-blocking points are tracked in #591 and will be addressed in a follow-up PR.

@oc-tmueller
oc-tmueller merged commit 36e9189 into master Oct 27, 2026
6 checks passed
@oc-tmueller
oc-tmueller deleted the fix/php-input-hardening branch October 27, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants