Open Build Service, one year later: command execution through Mercurial argument injection
In March 2025 we published an analysis of a remote code execution vulnerability in Open Build Service (OBS), tracked as CVE-2024-22033. A little over a year later we went back to the same attack surface and found a second, distinct flaw of the same family. It has now been reported to the openSUSE security team and fixed…
Série Supply Chain Attacks on Linux 4/4
- Supply Chain Attacks on Linux distributions - Overview
- Supply Chain Attacks on Linux distributions - OpenSUSE Open Build Service
- Supply Chain Attacks on Linux distributions - Fedora Pagure
- Open Build Service, one year later: command execution through Mercurial argument injection
An update to our March 2025 research on Open Open Build Service.
Where we left off
In March 2025 we published an analysis of a remote code execution vulnerability in Open Build Service (OBS), tracked as CVE-2024-22033. A little over a year later we went back to the same attack surface and found a second, distinct flaw of the same family. It has now been reported to the openSUSE security team and fixed. This post is a short refresher on the first finding, followed by the technical details of the new one.
Before the new bug, a quick reminder of why this software matters.
A reminder: what OBS is, and why it is a high-value target
Open Build Service is the packaging platform developed by openSUSE. It takes source code and package recipes and turns them into finished packages across many distributions and formats: RPM, DEB, Flatpak, AppImage, and more. It is not a niche tool; openSUSE itself is built on it, and it is used by projects and vendors such as VLC, Dell, and Intel.
Concretely, a package on OBS is described by a set of source files plus a small XML file named _service. That file lists source services: helper programs that OBS runs to fetch and prepare the sources before a build. Services can clone a Git or Mercurial repository, download a tarball, verify a checksum, rewrite a version string, and so on. Each service is a script under /usr/lib/obs/service/, and it is driven by parameters an author places in _service.
That design is what makes OBS interesting to attack. Anyone who can influence a package can influence what those services run. The threat model we worked under is the same as in 2025: the attacker controls the _service file and the committed package files, and the services execute server-side after a commit.
An RCE here is not an ordinary web bug. OBS sits at the root of a software supply chain. As we noted last year, an attacker who compromises the build infrastructure can reach toward the secrets used to sign packages, and from there push malicious packages to openSUSE users. Poisoning a single trusted package propagates to every machine that installs or updates it. That is why we keep coming back to this surface, and why a command-execution primitive on the OBS service server is treated as critical.
The 2025 finding, in one paragraph
CVE-2024-22033 lived in the download_url service. When that service is used with the download-manifest option, it builds a wget command line from attacker-controlled input without a proper separator, which allows extra wget arguments to be injected. Chained through wget configuration files, the argument injection led to command execution on the source service server. The root cause was a classic one: user-controlled data flowing into a command line as options rather than data.
The new bug is in a different service, with a different binary, and a different bypass, but the same root cause.
The new CVE-2026-56004: argument injection into Mercurial via obs_scm
The vulnerable service
The obs_scm service (and its tar_scm sibling) clones a source repository and prepares a tarball from it. It supports several backends through the scm parameter: git, svn, bzr, and hg (Mercurial). A typical, benign _service looks like this:
<services>
<service name="obs_scm">
<param name="scm">git</param>
<param name="url">https://example.org/project.git</param>
<param name="revision">v1.2.3</param>
</service>
</services>
The revision parameter tells the service which commit, tag, or revision to check out. It is controlled by whoever writes the _service file.
Root cause
The hg backend lives in /usr/lib/obs/service/TarSCM/scm/hg.py. The revision is consumed by switch_revision:
def switch_revision(self):
"""Switch sources to revision."""
if self.revision is None:
self.revision = 'tip'
cmd = self._get_scm_cmd() + ['update', self.revision]
rcode, _ = self.helpers.run_cmd(cmd,
cwd=self.clone_dir,
interactive=sys.stdout.isatty())
if rcode:
sys.exit('%s: No such revision' % self.revision)
The command is assembled as hg update <revision>, with the user-controlled revision appended directly and no -- separator between the update subcommand and the value. Mercurial therefore has no way to tell that the value is meant to be data rather than an option. If the revision starts with a dash, it is parsed as an hg command-line option.
Mercurial’s global --config option is all an attacker needs. It sets any configuration key on the command line, including hooks. Setting the pre-update hook runs an arbitrary shell command right before hg update does its work:
hg update --config=hooks.pre-update=<shell command>
The payload
The following _service triggers the flaw:
<services>
<service name="obs_scm">
<param name="scm">hg</param>
<param name="url">https://repo.mercurial-scm.org/hello</param>
<param name="revision">--config=hooks.pre-update=id > /tmp/idid</param>
</service>
</services>
Running this and tracing the service server with strace shows the injected option reaching Mercurial and, from there, a shell:
execve("/usr/lib/obs/service//obs_scm", ["/usr/lib/obs/service//obs_scm",
"--scm", "hg", "--url", "https://repo.mercurial-scm.org/hello",
"--revision", "--config=hooks.pre-update=id > /tmp/idid",
"--outdir", "/srv/obs/service/4909/out"], ...
execve("/usr/bin/hg", ["hg", "clone", "https://repo.mercurial-scm.org/hello",
"/srv/obs/service/4909/out/tmp6rp9wve4/hello"], ...
execve("/usr/bin/hg", ["hg", "update",
"--config=hooks.pre-update=id > /tmp/idid"], ...
execve("/bin/sh", ["/bin/sh", "-c", "id > /tmp/idid"], ...
The final execve is our command running in a shell on the service server.
The subtle part: why the space matters, and why Git is not affected
The Git backend builds its command lines the same way, yet it is not exploitable here. The reason is a detail in how the parameters travel to the service.
Both the server-side bs_service process and the osc client pass each service parameter as two separate tokens: the flag --revision, then the value. Python’s argparse normally rejects a value that begins with a dash, because it cannot tell it apart from an option. That check is what stops the equivalent Git payloads: a revision such as --output=... is refused before it ever reaches git.
Normally, argparse sees a word starting with a dash and thinks “that’s an option.” But it makes an exception: if the word contains a space, it figures “no real option looks like that” and treats it as a plain value instead of an option.
So a string like --config=hooks.pre-update=id > /tmp/id slips right through argparse, which lets it pass without complaint. Only later does hg read it again and understand it as the --config option.
The key detail is the space. Without a space, argparse blocks the word. With a space, it lets it through, and it ends up becoming a dangerous hg option.
Git, by contrast, is shielded by additional transport-level guards, and the argparse behavior does not help there: its useful injection points would need a dash-leading value with no space, which argparse rejects. Mercurial has no such guard, so --config goes straight through.
Preconditions
The attack is not entirely unconditional. To reach the hg update step:
- Mercurial must be installed on the service server and
scmset tohg. - The url must use an
http(s)scheme, which OBS’scheck_urlenforces. - The URL must point at a real, clonable Mercurial repository. The hook fires on
hgupdate, which only runs after a successful clone; the repository’s contents are irrelevant, an emptyhg initis enough. Public repositories such as https://repo.mercurial-scm.org/hello work. - The payload must contain a space, for the argparse reason above.
- A fresh URL avoids a poisoned SCM cache left by a previous failed clone.
We confirmed that the official openSUSE instance, build.opensuse.org, was vulnerable.
Impact
This is the most direct primitive of its kind: a plain commit carrying a valid _service file yields command execution on the OBS source service server, with no other file or additional service required. As stated in our report, an attacker who compromises the service server would be able to push malicious packages or modify legitimate packages, and so compromise openSUSE users.
The fix
We reported the issue to the openSUSE security team, who addressed it in a timely manner. The remediation is the standard one for argument injection: ensure user-controlled data cannot be interpreted as options. The change we recommended is to insert a -- separator between the update subcommand and the revision, so Mercurial always treats the revision as data (hg update -- <revision>). As a defense in depth measure, we also advised rejecting any revision value that begins with a dash before it reaches the SCM backend. The same reasoning applies to the other SCM backends that build command lines from user-controlled references.
Disclosure timeline
- 2026-06-29: Vulnerability discovered by Fenrisk and reported to the openSUSE security team.
- 2026-07-02: The OBS maintainers developed and applied a fix.
- 2026-07-02: CVE-2026-56004 identifier had been assigned at the time of reporting.
Keep reading
Supply Chain Attacks on Linux distributions - OpenSUSE Open Build Service
Open Build Service (OBS) is an open-source distribution development platform provided by openSUSE. It allows developers to manage the whole packaging process in order to build a package from a simple software source and…
Supply Chain Attacks on Linux distributions - Fedora Pagure
As discussed in the meta-article, we picked Pagure from the Fedora Apps Directory and already had a technical approach in mind. A software forge is likely to be a good target for an argument injection: we can expect the…
Supply Chain Attacks on Linux distributions - Overview
Supply chain attacks have been a trendy topic in the past years. Rather than directly attacking their primary target, attackers infiltrate less secure assets, such as software dependencies, firmware, or service…
