SevyDevy

Guide · Backend

Nextcloud Collabora Vulnerability 2026 Explained: Patch the Built-in CODE Server

18 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The Nextcloud Collabora vulnerability CVE-2026-110447 let an unauthenticated request run shell commands on the server, with no login. A CVE is a public ID for one security bug.
  • It is only in the Nextcloud app called Built-in CODE Server (richdocumentscode), which downloads and runs a copy of the document server on your own machine. A standalone Collabora server, including Collabora's own Docker image, packages and Nextcloud All-in-One, is not affected. Nextcloud itself running in Docker with this app installed is.
  • The app's proxy.php starts that document server when nothing is listening. Before version 26.4.303, text from the request ended up inside a double-quoted shell command, so a command written as $(...) in it ran as a real command.
  • The trap was subtle: the value was escaped, but escaping protects a shell argument, not a value pasted inside another quoted command. The 26.4.303 fix escapes the whole inner command instead.
  • You can stop here knowing you must upgrade richdocumentscode to 26.4.303 or later, and check for planted files if you ever ran an older version. Engineers get the exact source lines, a runnable demo of why double quotes leak, and the patch and compromise checks.

What goes wrong when a web app builds a shell command from a request?

A value that looks harmless can carry a command when the shell reads it. That is the Nextcloud Collabora vulnerability tracked as CVE-2026-110447, fixed in richdocumentscode 26.4.303.

Picture a common setup. A team self-hosts a file server so staff can edit documents in the browser. To open a document, a small helper starts a local document server on demand, then passes the browser's request to it. The setup is made up, but the bug is real.

The helper builds the start command as one line of shell text. Into that line it pastes a couple of values taken from the incoming request. It wraps each value with an escaping function first, so this feels safe.

Now ask the question the team skipped. Where exactly does the escaped value sit in that line? Here it sits inside a second quoted command handed to another shell. At that spot the escaping no longer holds, and a hidden command in the value runs.

Which words do you need before this vulnerability makes sense?

You need six: the built-in CODE server, proxy.php, the shell, command substitution, escaping, and unauthenticated. Each gets a plain meaning and an example from the team's setup.

Built-in CODE server. This is an optional Nextcloud app, richdocumentscode. Instead of pointing at a separate document server, it downloads one and runs it on the Nextcloud machine. Example: the team ticked a box and skipped running a second server.

proxy.php. This is one PHP file inside that app. It takes a web request, makes sure the document server is running, and forwards the request to it. Example: every document the staff open passes through this file.

The shell. The shell is the program that reads a command line and runs it, like bash or sh. It treats some characters as instructions, not plain text. Example: starting a background program is a shell job.

Command substitution. Writing a command inside a dollar sign and parentheses tells the shell to run that command first and drop its output in place. Example: a date wrapped that way becomes today's date before the main command runs. We call this a dollar-group below.

Escaping. Escaping rewrites a value so the shell treats it as one plain argument. PHP's escapeshellarg wraps a value in single quotes and neutralises quotes inside it. Example: a filename with a space stays one argument.

Unauthenticated. An unauthenticated request needs no login and no account. Anyone who can reach the address can send it. Example: a request to proxy.php never checks who you are before it acts.

What is the Nextcloud Collabora vulnerability in CVE-2026-110447?

It is an operating-system command injection in proxy.php in the Built-in CODE Server app, in every richdocumentscode version before 26.4.303. An unauthenticated request could make the server run a shell command as the web server user.

The flaw is reachable only while the local document server is not running. That is the case right after a restart, during an upgrade, or after a crash, because proxy.php then has to start the server itself.

The advisory rates it critical, CVSS v4.0 score 9.2, under CWE-78, the catalogue name for OS command injection. Its vector marks an attack requirement (AT:P), which fits the server-down condition. The CVE record says it was exploited in the wild in September and October 2026. It is a different bug from CVE-2025-66208, and the older fix does not stop it.

One detail makes it worse. proxy.php is a plain PHP file run straight from the app folder. We read the source: it loads none of Nextcloud's code and checks no login, so the usual Nextcloud guards never run for this file.

How did an escaped value still run a command?

Because the value was escaped for the wrong place. We read proxy.php at tag 26.4.302, the last release before the fix. In its start function it builds the launch command like this (names kept, line shortened):

text
$launchCmd = "bash -c \"( $appImage $remoteFontConfig "
              . "$IPv4only $lok_allow --pidfile=$pidfile || ... ) >/dev/null & disown\"";
exec($launchCmd, $output, $return);

Read the quotes carefully. The whole thing is a double-quoted PHP string. Inside it sits bash -c followed by another double-quoted string: the real command. Two of the pasted pieces are built from the request. lok_allow carries the Host header, and remoteFontConfig, added only on HTTPS, carries a URL made from the host and the request path.

Those two pieces do pass through escapeshellarg first. We confirmed that in the source. escapeshellarg wraps a value in single quotes, which is the right way to hand it to a shell as one argument.

But the value is not handed over as an argument. It is pasted inside the inner double-quoted command. PHP's exec gives the outer string to one shell. Inside double quotes, a single quote is just an ordinary character to that shell, so it runs any dollar-group it finds. The inner bash only sees the single quotes after the damage is done.

Where the parsing goes wrong

Notice where it breaks: inside double quotes the first shell treats the escaping's single quotes as plain characters and expands the dollar-group.

Diagram connections:

  • Request to proxy.php to proxy.php builds string (Starts the server)
  • proxy.php builds string to First shell reads line (exec(outer string))
  • First shell reads line to Dollar-group expands (Inside double quotes)
  • Dollar-group expands to Command runs as web user (Output pasted in)
  • Request to proxy.php: From client. CODE server is down
  • proxy.php builds string: From proxy. Pastes escaped value in
  • First shell reads line: From exec. Scans the double quotes
  • Dollar-group expands: From shell. Single quotes are plain text
  • Command runs as web user: From shell. No login was checked

Why does a dollar-group run inside double quotes but not single quotes?

Because the shell treats the two quote styles differently. Inside double quotes the shell still does command substitution and variable expansion. Inside single quotes it treats every character as literal text. This one rule is the whole bug class.

Here is a harmless check you can run. It never touches Nextcloud. It just prints what the shell does with the same text in each quote style.

bash
# Double quotes: the shell runs the inner command and pastes its output.
echo "value is $(echo EXPANDED)"
# Single quotes: the shell leaves it as plain text.
echo 'value is $(echo EXPANDED)'
# Single quotes placed INSIDE a double-quoted string do not protect it.
sh -c "echo 'value is $(echo EXPANDED)'"

Check it worked: run it with bash. The first line prints value is EXPANDED. The second prints value is $(echo EXPANDED), untouched. The third, where single quotes sit inside double quotes, prints value is EXPANDED again. That third case is the shape of the bug.

Now the same idea in PHP, with a value that is escaped first. The dangerous value here only tries to create an empty file named MARKER, so you can see whether the command ran. It sends nothing and attacks nothing.

php
<?php
$value = 'hello $(touch MARKER)';
$opt = '--url=' . escapeshellarg($value);   // wraps value in single quotes

// A. Like 26.4.302: escaped value pasted inside a double-quoted bash -c string.
exec("bash -c \"( echo $opt )\"");
echo file_exists('MARKER') ? "A: command ran\n" : "A: safe\n";
@unlink('MARKER');

// B. Like 26.4.303: escape the WHOLE inner command once more.
exec('bash -c ' . escapeshellarg("( echo $opt )"));
echo file_exists('MARKER') ? "B: command ran\n" : "B: safe\n";
@unlink('MARKER');

Check it worked: run it with PHP. Case A prints A: command ran, because the escaped value sits inside the inner double quotes, exactly the 26.4.302 pattern. Case B prints B: safe, because the inner command is escaped as one unit. We ran this on PHP 8.3.6 and saw that result.

How did richdocumentscode 26.4.303 fix it?

By escaping the whole inner command, not just the values inside it. We read proxy.php at tag 26.4.303. The start function now builds the command in two steps:

text
$innerCmd = "( $appImage $remoteFontConfig $IPv4only "
              . "$lok_allow --pidfile=$pidfile || ... ) >/dev/null & disown";
$launchCmd = "bash -c " . escapeshellarg($innerCmd);
exec($launchCmd, $output, $return);

Now escapeshellarg wraps the entire inner command as one argument to bash -c. The outer shell sees a single quoted blob and does no substitution inside it. The dollar-group in a pasted value stays plain text. Our case B above runs this exact shape and stays safe.

The change is small. The fix commit is 6cc8dae, titled tidy up how the coolwsd launch command is built, by the maintainer caolanm. The commit page shows one file changed, proxy.php, three lines added and one removed. A quiet title for a critical fix.

The newest release, 26.4.402, goes one step further. We read its source: it calls proc_open with an argument array. It keeps a fixed bash script, but passes each option as a separate argument, so bash never parses them as code. It also validates the Host header and no longer uses the request path.

Three ways proxy.php built the start command, and what a shell does with a pasted value
richdocumentscode versionHow the command is builtDoes a pasted dollar-group run?
26.4.302 (last before the fix)Value escaped, then pasted inside a double-quoted bash -c stringYes; the outer shell expands it
26.4.303 (the fix)Inner command built, then the whole command escaped as one argumentNo; the shell sees one quoted blob
26.4.402 (latest)proc_open with an argument array; fixed bash script, each option a separate argumentNo; values are never parsed as code

You can stop here if you never had the Built-in CODE app installed. If you ran a version before 26.4.303 at any point since about 10 September, read the compromise checks below, even if you have patched. Attacks began a day before the fix shipped.

Who is affected: built-in CODE versus standalone Collabora?

Only one setup is affected: the Nextcloud app Built-in CODE Server, richdocumentscode, before 26.4.303. That is the app that downloads and runs the document server on the Nextcloud machine itself.

The advisory says standalone Collabora Online servers are not affected. Collabora's blog adds its Docker images, RPM and DEB packages, and Nextcloud All-in-One. The Nextcloud forum warning adds Nextcloud Office on its own, with richdocumentscode not installed.

Note the scope of Docker. It means Collabora's own image. A Nextcloud server that itself runs in Docker is affected if the Built-in CODE app is installed, and the advisory lists a container restart as a trigger. The Hacker News quotes the store listing as for local testing and non-production use, and Collabora's blog calls the app focused on home users.

One group is stuck. Nextcloud 29 and older are end of life, and the fixed app needs Nextcloud 30 or later. As of 23 October 2026 the app store offers Nextcloud 25 to 29 version 26.4.104, which is still affected, so they cannot patch by updating the app. They must upgrade Nextcloud or remove the app.

Which setups the bug can reach

Notice that the risk follows the app, not the host type: Nextcloud in Docker with the built-in app is still affected.

Diagram connections:

  • Nextcloud server to Built-in CODE app (Affected before 26.4.303)
  • Nextcloud server to Standalone Collabora (Not affected)
  • Standalone Collabora to Collabora Docker, DEB, AIO (Deployed as)
  • Nextcloud server: From browser. VM, bare metal or Docker
  • Built-in CODE app: From Nextcloud. Runs proxy.php here
  • Standalone Collabora: From Nextcloud. Separate server
  • Collabora Docker, DEB, AIO: From admin. No proxy.php path

How do you check your richdocumentscode version and patch?

Read the installed version with Nextcloud's command line tool, occ, run as the web server user. That user is www-data on Debian and Ubuntu; other systems may use apache or nginx. In the official Nextcloud Docker image, run docker exec -it --user www-data CONTAINER_ID php occ instead.

  1. List the app and its version

    Run sudo -E -u www-data php occ app:list and find the line that reads richdocumentscode followed by its version. We checked the occ source: it prints each app as name and version. Confirm 26.4.303 or higher.

  2. Update the app if it is older

    Run sudo -E -u www-data php occ app:update richdocumentscode, or update it from the apps page. On Nextcloud 29 or older the store has no fixed build, so this will not help you.

  3. Remove it if you do not need it

    If the app was only for testing, first run the integrity check in the next section, because removing the app deletes its folder and any planted file with it. Then run sudo -E -u www-data php occ app:remove richdocumentscode. The advisory says disabling is not enough, so remove or upgrade; do not just disable.

  4. Prefer a standalone server for production

    If staff need document editing in production, run a standalone Collabora server in Docker or from a package, and point Nextcloud Office at it. That setup never had this flaw.

How do you check whether your server was already hit?

Patching closes the door, but it does not undo an earlier break-in. Attacks were logged from about 10 September, weeks before the public warning. If your app ran a vulnerable version while the document server was down, check for signs of compromise.

The advisory gives no detection steps, so use the checks affected admins reported in GitHub issue 354 and the Nextcloud forum. Nextcloud's own integrity check is the strongest, because it knows every file that should be in an app.

  1. Run the integrity check

    Run sudo -E -u www-data php occ integrity:check-app richdocumentscode, then occ integrity:check-core for the server files. The manual says EXTRA_FILE means a file on disk is not in the signed list. A forum user found a planted file this way.

  2. Look for unexpected PHP files

    Reporters found planted files named shx*.php and shrf*.php in apps/richdocumentscode, and a forum user found Nx_*.php there. Start in that folder, then look for recently changed PHP files elsewhere in the web root.

  3. Review web server access logs

    Search access logs back to at least 10 September. Requests to proxy.php?status are normal polling, so do not panic at them. Flag ones whose path also has encoded dot-dot segments, and any request for an unfamiliar PHP file in the app folder.

  4. Check for odd account activity

    One admin found the break-in only from failed sudo alert emails sent by the web server account. Look for sudo attempts or other actions by the www-data user that you did not expect.

  5. Rotate anything the web user could read

    The first reporter in issue 354 advised treating anything the web server account could read as exposed. If you find a planted file, change every such secret, remove the file, and keep investigating.

How do you explain this vulnerability in a system design interview?

Keep it to the mechanism and the lesson, not the payload. Walk through where the untrusted value travelled and which layer parsed it. These five steps give a clean frame.

  1. Name the bug class

    Call it OS command injection, CWE-78. A request value reached a shell as a command, with no login needed, because proxy.php runs outside Nextcloud's guards.

  2. Locate the exact slip

    The value was escaped with escapeshellarg, but then pasted inside a second double-quoted command. A shell parsed it again there, where the escaping did not apply.

  3. State the trigger window

    It only fires while the local document server is down, during a restart, upgrade, or crash, because that is when proxy.php builds the start command.

  4. Give the fix and the sturdier option

    Escape the whole inner command as one argument, as 26.4.303 does, or better, pass each value as a separate argument to a fixed script, as 26.4.402 does.

  5. Draw the general lesson

    Know which layer parses your value. Escaping for a shell argument is useless once the value is embedded in another quoted command parsed by a shell.

Questions people ask

What is CVE-2026-110447 in simple terms?
It is a critical flaw in the Nextcloud Built-in CODE Server app, richdocumentscode, before version 26.4.303. Its proxy.php file built a shell command from request text. An unauthenticated request could run a command on the server, but only while the local document server was not running, such as during a restart or upgrade. The fix ships in 26.4.303.
Am I affected if I use standalone Collabora or Docker?
Not if Collabora runs as its own server. The advisory says standalone Collabora Online is not affected, and Collabora's blog adds its Docker images, packages and Nextcloud All-in-One. But Nextcloud itself running in Docker is affected if the Built-in CODE app is installed. The risk follows the app, not the host.
Is disabling the app enough to be safe?
No. The advisory states plainly that disabling the app is not sufficient. Our reading of the source is that proxy.php is a plain file the web server still serves, and loads no Nextcloud code. You must upgrade richdocumentscode to 26.4.303 or later, or remove the app entirely. On Nextcloud 29 or older you cannot install the fix, so remove the app or upgrade Nextcloud first.
Which version should I run, 26.4.303 or 26.4.402?
Both contain the fix. 26.4.303 from 11 September is the version the advisory names, and it fixes the bug by escaping the whole command. 26.4.402, dated 24 September by The Hacker News, is the newest and passes options as separate arguments, which is sturdier. Some admins reported start-up problems on 26.4.402, in GitHub issues 355 and 356, and rolled back to 26.4.303. As of 23 October 2026 no newer release had appeared.
How do I check if my server was already attacked?
Run the integrity check with occ integrity:check-app richdocumentscode; an EXTRA_FILE result flags a planted file. Also look for unexpected PHP files in the app folder. Review access logs back to about 10 September for odd paths, not routine status polls, and watch for sudo attempts by the web server user. Run these before removing the app. If you find a planted file, change every secret the web user could read.
Why did escaping the value not prevent the bug?
Because escapeshellarg protects a value only when you pass it as a shell argument. In the vulnerable code the escaped value was pasted inside a second double-quoted command, which a shell parsed again. At that point the single quotes from escaping were just text, and a command substitution group in the value ran. The lesson is to know which layer parses your value.

Sources and further reading

Practise this with Lala

Practise