Summary
ClusterFuzz builds a shell command by string-concatenating an extracted ZIP
path into a subprocess.run(..., shell=True) call. A ZIP whose directory name
contains $() causes Bash to execute the injected command on the trusted host
process before the fuzz target ever runs, bypassing the isolation boundary
between the untrusted runner and the host.
Affected versions
- v2.36.2, v2.37.0, v2.37.1 (tested at revision 7758497)
- Unpatched as of Aug 23, 2026
Root cause
In adb.py, get_package_name() constructs the aapt command by concatenation:
# Vulnerable pattern
aapt_command = '%s dump badging %s' % (aapt_path, apk_path)
subprocess.run(aapt_command, executable='/bin/bash', shell=True)
apk_path comes from an extracted ZIP. If the ZIP contains a directory named
payload$(touch /tmp/pwned), that name is preserved on disk and lands verbatim
in the shell command:
/resources/aapt dump badging /build/custom/payload$(touch /tmp/pwned)/app.apk
Bash evaluates $(touch /tmp/pwned) on the host while processing the word,
before aapt runs. The single quotes visible in write_command_line_file do not
protect against this because they are inside an outer double-quoted word.
Who can trigger this
Requires privileged ClusterFuzz project access, specifically the role that can
create or update fuzzing jobs and upload custom builds. Standard testcase
uploaders cannot trigger this.
Why the boundary matters
ClusterFuzz's security model isolates fuzz target code in an untrusted runner,
separate from the trusted host process that holds credentials, task metadata,
and cluster state. A project operator is permitted to supply code for the
untrusted runner. This bug lets that same operator run code in the trusted host
process, a boundary the design is supposed to prevent.
Proof of concept
The PoC extracts the exact vulnerable functions from the pinned source and runs
them with a harmless fake aapt. No Android device required.
docker build -f poc/Dockerfile -t clusterfuzz-cmdi-poc poc
docker run --rm -v "$(pwd)/clusterfuzz:/src:ro" clusterfuzz-cmdi-poc
Expected output:
CONTROL_MARKER_CREATED=False ← normal path, no injection
EXPLOIT_MARKER_CREATED=True ← $() in directory name executed on host
APP_PATH_CONTAINS_COMMAND_SUBSTITUTION=True
CONTROL_MARKER_CREATED=False confirms a normal path goes through the same
code without triggering anything. EXPLOIT_MARKER_CREATED=True confirms the
injected directory name causes Bash to execute on the host while package
inspection completes normally.
Fix
Replace string concatenation + shell=True with an argument list + shell=False:
## get_package_name() - safe version
result = subprocess.run(
[aapt_path, 'dump', 'badging', apk_path],
shell=False,
capture_output=True,
text=True
)
# write_command_line_file() - write via Python file I/O, not shell
with open(command_line_path, 'w') as f:
f.write(apk_path)
apdk_path becomes a list element, never concatenated into a shell string.
Archive paths should be treated as untrusted regardless of uploader privilege level.
Disclosure timeline
- Aug 23, 2026 - Reported to Google VRP
- Aug 24, 2026 - Google confirmed the issue is real but below their security
escalation threshold; suggested public disclosure on GitHub
Summary
ClusterFuzz builds a shell command by string-concatenating an extracted ZIP
path into a
subprocess.run(..., shell=True)call. A ZIP whose directory namecontains
$()causes Bash to execute the injected command on the trusted hostprocess before the fuzz target ever runs, bypassing the isolation boundary
between the untrusted runner and the host.
Affected versions
Root cause
In
adb.py,get_package_name()constructs the aapt command by concatenation:apk_path comes from an extracted ZIP. If the ZIP contains a directory named
payload$(touch /tmp/pwned), that name is preserved on disk and lands verbatim
in the shell command:
/resources/aapt dump badging /build/custom/payload$(touch /tmp/pwned)/app.apk
Bash evaluates $(touch /tmp/pwned) on the host while processing the word,
before aapt runs. The single quotes visible in write_command_line_file do not
protect against this because they are inside an outer double-quoted word.
Who can trigger this
Requires privileged ClusterFuzz project access, specifically the role that can
create or update fuzzing jobs and upload custom builds. Standard testcase
uploaders cannot trigger this.
Why the boundary matters
ClusterFuzz's security model isolates fuzz target code in an untrusted runner,
separate from the trusted host process that holds credentials, task metadata,
and cluster state. A project operator is permitted to supply code for the
untrusted runner. This bug lets that same operator run code in the trusted host
process, a boundary the design is supposed to prevent.
Proof of concept
The PoC extracts the exact vulnerable functions from the pinned source and runs
them with a harmless fake aapt. No Android device required.
docker build -f poc/Dockerfile -t clusterfuzz-cmdi-poc poc
docker run --rm -v "$(pwd)/clusterfuzz:/src:ro" clusterfuzz-cmdi-poc
Expected output:
CONTROL_MARKER_CREATED=False ← normal path, no injection
EXPLOIT_MARKER_CREATED=True ← $() in directory name executed on host
APP_PATH_CONTAINS_COMMAND_SUBSTITUTION=True
CONTROL_MARKER_CREATED=False confirms a normal path goes through the same
code without triggering anything. EXPLOIT_MARKER_CREATED=True confirms the
injected directory name causes Bash to execute on the host while package
inspection completes normally.
Fix
Replace string concatenation + shell=True with an argument list + shell=False:
apdk_path becomes a list element, never concatenated into a shell string.
Archive paths should be treated as untrusted regardless of uploader privilege level.
Disclosure timeline
escalation threshold; suggested public disclosure on GitHub