Skip to content

fix: prevent appconfig path traversal to RCE via public_/remote_ keys (OC10-146) - #41804

Open
oc-tmueller wants to merge 1 commit into
masterfrom
fix/oc10-146-appconfig-path-traversal-master
Open

fix: prevent appconfig path traversal to RCE via public_/remote_ keys (OC10-146)#41804
oc-tmueller wants to merge 1 commit into
masterfrom
fix/oc10-146-appconfig-path-traversal-master

Conversation

@oc-tmueller

Copy link
Copy Markdown

Summary

An authenticated admin could set the core appconfig key public_webdav (or any public_/remote_ key) to a path-traversal value and have it require_once'd by public.php on the next GET /public.php/webdav, achieving remote code execution.

Two independent defects made this possible; both are fixed here (defense in depth).

1. Sink — public.php had no traversal check (essential fix)

public.php included the stored handler path relative to the app directory with no traversal guard, unlike remote.php which already rejects ../. Added the same guard so a traversal path can never be included — this is the DB- and endpoint-independent gate.

2. Guard bypass — strict $app === 'core' compare

AppConfigController blocked admins from setting public_/remote_ keys on core with a strict $app === 'core' compare. A mangled app id such as "core " (trailing space) is not equal to "core" in PHP, yet the database folds it back to the core row — defeating the guard. This is the same defeat mechanism as the earlier core%81 truncation bypass. The app id is now normalized (cleanAppId + trim + strtolower) before the check, in:

  • AppConfigController::getValue/setValue/deleteKey
  • the legacy core/ajax/appconfig.php endpoint
  • and deleteApp now refuses to wipe the whole core appconfig.

Tests

Adds regression tests covering the mangled core spellings ("core ", " core", "CORE", "core/", "core..") and allowed near-misses (encore, non-service keys on core).

Verified locally in owncloudci/php:8.3 on sqlite, mirroring the php-unit workflow (fresh maintenance:install per run, then tests/drone/test-phpunit.sh's phpunit invocation):

  • Full unit suite: OK — 11075 tests, 60389 assertions, 91 skipped (the 91 skips are the usual "running tests as root" / DB-specific ones).
  • tests/Settings/Controller/AppConfigControllerTest.php: OK — 40 tests, 105 assertions.
  • Negative control: restoring the strict $app === 'core' compare makes 17 of the new cases fail, confirming they catch the bypass.
  • calens renders the changelog entry as a Security item and exits 0.

Note the public.php sink fix has no unit-test coverage — public.php is a top-level entry script with no unit tests. It mirrors the existing, reviewed guard in remote.php verbatim.


Forward-port of #41803 to master. That PR targets the 10.16 release branch; master carries the identical vulnerable code. Cherry-picked with no adaptation — the code diff is byte-identical to the reviewed one (only the changelog entry is renumbered to this PR). The one master-side difference in public.php, an added OC_Util::tearDownFS() line, sits below the guard's insertion point and did not conflict.

@oc-tmueller
oc-tmueller requested a review from a team as a code owner September 3, 2026 11:03
… (OC10-146)

An authenticated admin could set the core appconfig key `public_webdav`
(or any `public_`/`remote_` key) to a path-traversal value and have it
`require_once`'d by public.php on the next `GET /public.php/webdav`,
achieving remote code execution.

Two independent defects made this possible:

1. Sink: public.php included the stored handler path relative to the app
   directory with no traversal check, unlike remote.php which already
   rejects `../`. This is the essential, DB-independent gate: add the same
   guard so a traversal path can never be included.

2. Guard bypass: AppConfigController used a strict `$app === 'core'` compare
   to block admins from setting `public_`/`remote_` keys on core. A mangled
   app id such as `"core "` (trailing space) is not equal to `"core"` in PHP
   yet the database folds it back to the core row, defeating the guard. This
   is the same defeat mechanism as OC10-5 (`core%81` truncation). Normalize
   the app id (cleanAppId + trim + strtolower) before the check, in both the
   controller (getValue/setValue/deleteKey) and the legacy core/ajax/appconfig.php
   endpoint, and block deleting the whole core appconfig via deleteApp.

Adds regression tests covering the mangled `core` spellings and the
allowed near-misses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
@oc-tmueller
oc-tmueller force-pushed the fix/oc10-146-appconfig-path-traversal-master branch from a47964f to dd2f58a Compare September 3, 2026 11:04

@phil-davis phil-davis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

Comment thread core/ajax/appconfig.php
// mangled spellings which the database folds back to the "core" row
// (e.g. "core " with a trailing space) cannot slip past the guard. See OC10-146.
$normalizedApp = isset($app) ? \strtolower(\trim((string)$app)) : $app;
if ($normalizedApp === 'core' && isset($_POST['key']) &&(\substr((string)$_POST['key'], 0, 7) === 'remote_' || \substr((string)$_POST['key'], 0, 7) === 'public_')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I thought that code-style would complain about &&( (no space), but that was already in the previous code anyway.

['CORE', 'public_key1', 'foo'],
['Core', 'remote_key1', 'foo'],
['core/', 'public_key1', 'foo'],
['core..', 'remote_key1', 'foo'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: OC_App::cleanAppId removes junk like .. so this test case is also equivalent to trying to set a key for "core".

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