HexBrain

Weblate · Report #3874004

Incomplete fix for CVE-2022-23915: Mercurial argument injection in HgRepository.get_file() leads to command execution

Validstatus
highseverity
34upvotes
15 Sept 2026disclosed

Summary

I discovered an authenticated RCE in Weblate caused by a residual incomplete fix for CVE-2022-23915.

Weblate passed repository-controlled filenames to Mercurial without placing them after the -- option terminator. As a result, filenames beginning with - could be interpreted as Mercurial command-line options instead of literal repository paths.

A user with project-scoped component.edit permission could abuse this behavior to inject Mercurial configuration and replace the expected hg cat operation with an attacker-controlled shell command. When Weblate later processed the repository through the RESX update path, the command executed with the privileges of the Weblate service account.

The vulnerability affected Weblate 4.11.1 through 2026.7.1 and was later assigned CVE-2026-86035.

How it was found

Summary

Weblate passes the configured component template filename to hg cat without an option terminator:

return self.execute(
    ["cat", "--rev", revision, path],
    remote_op="none",
    needs_lock=False,
    merge_err=False,
)

Component.template accepts relative filenames beginning with -. Mercurial therefore interprets an option-shaped template filename as command-line configuration instead of a repository path.

The production Update RESX files add-on passes component.template directly into this sink after a repository update:

content = component.repository.get_file(
    component.template,
    previous_head,
)

Using Mercurial's --config=alias.cat=!<command> syntax, an attacker can replace hg cat with a shell alias and execute commands as the Weblate service account.

This appears to be an incomplete fix for CVE-2022-23915 / GHSA-3872-f48p-pxqj. The Weblate 4.11.1 fix hardened several Mercurial argument paths but did not fix HgRepository.get_file().

Affected versions

The residual vulnerability was confirmed in:

  • Weblate 4.11.1, the first release containing the CVE-2022-23915 fix.
  • Latest PyPI release 2026.7.1.
  • Current main commit 76d311849a56c99cfc68968827a7cf9a13af3551.

The underlying sink and reachable caller first appeared in Weblate 2.19. Versions before 4.11.1 are already covered by CVE-2022-23915. Therefore, the incomplete-fix range is 4.11.1 through the current release and main.

Preconditions

The component must:

  1. Use Mercurial.
  2. Use RESX.
  3. Contain an option-shaped template filename.
  4. Have the Update RESX files add-on installed.
  5. Process a later repository update.

The integration proof demonstrates that a non-superuser, non-staff account with project-scoped component.edit can set the template and install the compatible add-on through the real API even though management.addons is false.

The add-on permission behavior is included only to prove reachability; I am not reporting it as a separate vulnerability.

Steps to reproduce

Download these two attachments into the same directory:

  • run_full_integration_standalone.sh
  • release_2026_7_1_full_integration_test.py

Then run:

chmod +x run_full_integration_standalone.sh
./run_full_integration_standalone.sh

The runner:

  1. Downloads the official Weblate 2026.7.1 PyPI source distribution.
  2. Verifies SHA-256 b0bcfea9f9143dc43e652ee317f6dcc85218fa9bc283445ceee98cd2b51462c7.
  3. Creates an ephemeral PostgreSQL 14 database.
  4. Uses the official weblate/weblate:2026.7.1.1 container.
  5. Executes four real Weblate integration tests.
  6. Removes its temporary containers, network and files.

The proof exercises the real:

  • Component REST API.
  • Add-on installation REST API.
  • Weblate permission backend.
  • PostgreSQL-backed records.
  • Component.do_update().
  • Post-update add-on dispatcher.
  • ResxUpdateAddon.update_translations().
  • HgRepository.get_file().

Authorization boundary demonstrated

Before component.edit was granted:

HTTP/1.1 403 Forbidden

After granting only project-scoped component.edit:

restricted_user_superuser=False
restricted_user_staff=False
component_edit=True
management_addons=False
api_template_patch=200
api_addon_install=201
addon_post_update_event=True

Vulnerable result

Both independent vulnerable runs produced:

normal_do_update=True
synthetic_read='WEBLATE-OWNED-SYNTHETIC-INTEGRATION-PROOF\n'
execution_identity='uid=1000(weblate) gid=1000(weblate) ...\n'

This proves backend command execution as the Weblate service account. It is not inferred from a parser error or reflected input.

Controls

The benign safe.resx control completed the same normal update path without creating either controlled output.

The patched control changed only the vulnerable vector to:

["cat", "--rev", revision, "--", path]

Result:

patched_normal_do_update=True
patched_synthetic_read=None
patched_execution_identity=None

Final test result:

Ran 4 tests
OK

Expected behavior

Repository filenames must be placed after an explicit -- option terminator. An option-shaped filename must be handled as a literal path and must never alter Mercurial configuration or command dispatch.

Observed behavior

The accepted component template reaches Mercurial before an option terminator. Mercurial evaluates it as --config, invokes the controlled shell alias as the Weblate service account, reads the synthetic file, and returns success from the normal RESX update.

Recommended remediation

Fix HgRepository.get_file() as follows:

return self.execute(
    ["cat", "--rev", revision, "--", path],
    remote_op="none",
    needs_lock=False,
    merge_err=False,
)

I also recommend auditing every Git and Mercurial call containing repository-derived path arguments and adding a regression test using an option-shaped tracked filename.

Prior issues and duplicate review

This is not the same as CVE-2026-24126, which involved admin-only ssh-keyscan argument injection and file disclosure through /manage/ssh/.

This report concerns HgRepository.get_file(), a project-scoped component editor, the RESX update path, and command execution through a Mercurial alias.

I found the original CVE-2022-23915 hardening PR but no public report covering this missed get_file() sink.

Safety statement

Testing used only local source, official packages and containers, temporary repositories, ephemeral databases, and researcher-created synthetic data. No Hosted Weblate instance, third-party data, credentials, or system file such as /etc/passwd was accessed.

Impact

An authenticated user with project-scoped component-edit permission can cross the intended Weblate authorization boundary and execute operating-system commands with the privileges of the shared Weblate service account.

Depending on deployment configuration, this can expose or modify repositories, application data, environment-provided credentials, signing material, translation artifacts, or other resources accessible to the Weblate process. It may also allow tampering with repository contents or generated translation output.

The safe proof demonstrated repeatable command execution as uid=1000(weblate) and reading of a researcher-created synthetic file. It does not claim root execution. The exact -- delimiter fix prevented the effect while preserving the normal repository update.

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