Back to skill

Security audit

gitlab-code-reviewer

Security checks for vulnerabilities and agentic risk

Overview

This GitLab review skill is coherent, but it can expose your GitLab token to a host taken from the MR URL and can post comments with your account.

Install only if you trust the MR URLs you will provide and understand that the skill may use your GitLab token to read private MR data and post comments. Prefer a least-privilege, short-lived token, do not use untrusted or http MR URLs, and fix host validation before use so the token is sent only to the configured GitLab origin.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gitlab_client.py:38
Finding
GitLab Token Can Be Exfiltrated to an Attacker-Controlled Host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gitlab_client.py:38-78, 122-125, 142-144, 196-199` **Vulnerability Type**: Credential disclosure through unvalidated network destination **Risk Level**: Critical ### Vulnerable Code ```python def parse_mr_url(url: str) -> tuple[str, str, str]: """ Parse a GitLab MR URL into (host, project_path_encoded, mr_iid). Supports: https://gitlab.com/group/subgroup/project/-/merge_requests/123 https://gitlab.example.com/namespace/project/-/merge_requests/456 Returns (host, url_encoded_project_path, mr_iid). """ pattern = r"(https?://[^/]+)/(.+)/-/merge_requests/(\d+)" m = re.match(pattern, url.rstrip("/")) if not m: raise ValueError(f"Cannot parse MR URL: {url}") host = m.group(1) project_path = m.group(2) mr_iid = m.group(3) encoded_path = project_path.replace("/", "%2F") return host, encoded_path, mr_iid def api_get(host: str, token: str, path: str, params: dict | None = None) -> dict | list: url = f"{host}/api/v4{path}" if params: url += "?" + urlencode(params) req = Request(url, headers={"PRIVATE-TOKEN": token, "Content-Type": "application/json"}) try: with urlopen(req) as resp: return json.loads(resp.read().decode()) except HTTPError as e: body = e.read().decode() raise RuntimeError(f"GitLab API error {e.code} on GET {path}: {body}") from e ``` The vulnerable data flow is invoked as follows: ```python def fetch_mr(mr_url: str) -> dict: """Return MR metadata (title, author, source_branch, target_branch, description, state).""" creds = load_credentials() host, project, iid = parse_mr_url(mr_url) data = api_get(host, creds["token"], f"/projects/{project}/merge_requests/{iid}") ``` ### Technical Analysis The Skill loads a privileged GitLab token from `~/.openclaw/credentials/gitlab.json`, but it does not bind that token to the configured GitLab ho ...[truncated 1865 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Normalize the `host` value from the credential file and use it as the exclusive API destination. 2. Parse the user-supplied MR URL with `urllib.parse.urlsplit`. 3. Require the URL scheme to be exactly `https`. 4. Compare the normalized scheme, hostname, and effective port against the configured host before loading or transmitting the token. 5. Reject URLs containing user information, unexpected ports, fragments, or origins that do not exactly match the configured GitLab origin. 6. Avoid redirects to different origins, or implement a redirect handler that rejects every cross-origin redirect while carrying credentials. 7. Add tests proving that arbitrary hosts, HTTP URLs, deceptive subdomains, and alternate ports cannot receive the token. 8. Revoke and replace any token that may already have been used with an untrusted MR URL. A safe design should resemble: ```python from urllib.parse import urlsplit def validate_mr_origin(mr_url: str, configured_host: str) -> None: supplied = urlsplit(mr_url) configured = urlsplit(configured_host) if supplied.scheme != "https": raise ValueError("MR URLs must use HTTPS") if ( supplied.hostname != configured.hostname or supplied.port != configured.port or supplied.username is not None or supplied.password is not None ): raise ValueError("MR URL origin does not match the configured GitLab host") def fetch_mr(mr_url: str) -> dict: creds = load_credentials() validate_mr_origin(mr_url, creds["host"]) _, project, iid = parse_mr_url(mr_url) return api_get( creds["host"].rstrip("/"), creds["token"], f"/projects/{project}/merge_requests/{iid}", ) ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:99
Finding
Review Content Is Written to a Predictable Shared Temporary File<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:99-116` **Vulnerability Type**: Unsafe temporary-file handling and sensitive-data persistence **Risk Level**: Medium ### Vulnerable Code ```bash # 1. Write all findings to a JSON file cat > /tmp/mr_comments.json << 'EOF' [ { "file_path": "src/main/UserService.java", "line": 42, "body": "[CRITICAL] Transaction wraps HTTP call...\n\nSuggestion:\n```java\n// fix\n```" } ] EOF # 2. Post via script python scripts/post_comments.py <mr_url> /tmp/mr_comments.json ``` ### Technical Analysis The documented workflow directs the Agent to write review comments to the fixed path `/tmp/mr_comments.json`. Comment bodies may contain excerpts or corrected versions of proprietary source code. The fixed name creates a race condition in a shared temporary directory. Shell redirection follows an existing symbolic link, so another local user may pre-create that path as a symlink. The workflow also does not set restrictive permissions or remove the file after posting. As a result, review content can remain on disk beyond the Skill run, contrary to the stated rule not to persist source-code content. ### Attack Path 1. A local attacker anticipates execution of the Skill. 2. The attacker monitors the predictable file path or pre-creates `/tmp/mr_comments.json` as a symbolic link. 3. The workflow writes review comments and possible source snippets to that path. 4. The attacker reads the resulting file where local permissions permit, or causes the invoking user to overwrite another file reachable through the symlink. 5. Because no cleanup is specified, the content may remain available after the review completes. ### Impact Assessment The primary impact is local disclosure of MR findings and proprietary source-code fragments. In a multi-user environment, another local account may obtain data from private repositories. Symlink exploitation may also overwrite a file writable by the invoking account. Th ...[truncated 162 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the fixed path with a securely generated temporary file. 2. Create the file atomically with mode `0600`. 3. Do not follow pre-existing symbolic links. 4. Delete the file in a guaranteed cleanup block after posting, including error paths. 5. Prefer passing comments directly through an in-memory API where practical. 6. Avoid including unnecessary source excerpts in persisted comment data. The posting script can accept standard input, or the caller can use Python’s `tempfile` module: ```python import json import os import tempfile path = None try: with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", prefix="mr-comments-", suffix=".json", delete=False, ) as f: os.chmod(f.name, 0o600) json.dump(comments, f, ensure_ascii=False) path = f.name post_comments(path) finally: if path is not None: try: os.unlink(path) except FileNotFoundError: pass ``` The documentation should explicitly require secure creation and guaranteed deletion rather than instructing the Agent to use a global fixed filename. ]]>

T05 · Unauthorized Access and Privilege Escalation

Note
Location
scripts/ignore_matcher.py:51
Finding
Ignore Matcher Unnecessarily Reads the Token-Bearing Credential File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ignore_matcher.py:51-58` **Vulnerability Type**: Excessive credential access and least-privilege violation **Risk Level**: Low ### Vulnerable Code ```python def _load_credential_patterns() -> list[str]: if not CREDS_PATH.exists(): return [] try: with open(CREDS_PATH) as f: creds = json.load(f) return creds.get("ignore_patterns", []) except Exception: return [] ``` ### Technical Analysis The ignore matcher only needs non-secret filename patterns, but it opens and parses the same JSON object that contains the GitLab token and host. This unnecessarily places a credential-bearing object in the filtering component’s process memory and expands the number of code paths that require access to secret storage. No direct network transmission or logging of the token was identified in `ignore_matcher.py`. The issue is therefore a least-privilege design weakness rather than a demonstrated credential leak. Nevertheless, local filtering should not require access to an API token. The blanket `except Exception` also conceals malformed configuration and permission errors, making misuse or configuration failures harder to detect. ### Attack Path 1. The ignore matcher is invoked to classify an MR file. 2. It opens `~/.openclaw/credentials/gitlab.json`. 3. The complete credential object, including the token, is loaded into process memory even though only `ignore_patterns` is needed. 4. A future defect, debugging hook, compromised runtime component, or instrumentation in this otherwise non-secret processing path could access the token-bearing object. There is no confirmed direct exfiltration path in the current matcher implementation; the risk is the avoidable expansion of credential exposure. ### Impact Assessment The matcher gains read access to a GitLab token that is not necessary for its filtering function. If this unnecessary access is combined with ...[truncated 265 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Move `ignore_patterns` to a separate non-secret configuration file, such as `~/.config/openclaw/gitlab-review.json`. 2. Restrict `gitlab.json` to the API client that actually needs the token. 3. Prefer passing sanitized ignore patterns into `filter_diffs()` from a trusted configuration layer. 4. Parse configuration into a dedicated structure that never contains the token. 5. Catch specific exceptions and report malformed configuration rather than silently suppressing every failure. 6. Apply restrictive filesystem permissions to the credential file independently of the non-secret review configuration. For example: ```python CONFIG_PATH = Path.home() / ".config" / "openclaw" / "gitlab-review.json" def load_ignore_patterns() -> list[str]: if not CONFIG_PATH.exists(): return [] try: with open(CONFIG_PATH, encoding="utf-8") as f: config = json.load(f) except (OSError, json.JSONDecodeError) as exc: raise RuntimeError(f"Cannot load ignore-pattern configuration: {exc}") from exc patterns = config.get("ignore_patterns", []) if not isinstance(patterns, list) or not all(isinstance(p, str) for p in patterns): raise ValueError("ignore_patterns must be a list of strings") return patterns ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (7)

Credential Access

High
Category
Privilege Escalation
Content
+ ").\n"
                "Analysis will proceed, but inline comments CANNOT be posted.\n"
                "To enable posting: regenerate the token with scope 'api' at\n"
                "GitLab → User Settings → Access Tokens, then update\n"
                "~/.openclaw/credentials/gitlab.json.",
                file=sys.stderr,
            )
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
+ ").\n"
                "Analysis will proceed, but inline comments CANNOT be posted.\n"
                "To enable posting: regenerate the token with scope 'api' at\n"
                "GitLab → User Settings → Access Tokens, then update\n"
                "~/.openclaw/credentials/gitlab.json.",
                file=sys.stderr,
            )
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The description says the skill performs code review on GitLab merge requests, but it does not clearly disclose that it will read credentials from a local file and may post comments to the remote GitLab MR. This weakens informed user consent and increases the risk of surprising outbound actions performed with stored tokens.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger phrases are broad enough to match common user requests like 'review this MR' or 'check this merge request', which can cause the skill to activate in situations where the user did not explicitly consent to GitLab API access or outbound actions. In this skill, unintended invocation is more dangerous because the workflow includes reading stored credentials and potentially posting comments back to GitLab.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
```

**Constraints:**
- Do not auto-approve the MR.
- Do not add labels or trigger pipelines.
- Only post comment-type discussions (no approval API calls).
- If a line is not in the diff, the API returns an error — log it and continue with the next comment.
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
**Constraints:**
- Do not auto-approve the MR.
- Do not add labels or trigger pipelines.
- Only post comment-type discussions (no approval API calls).
- If a line is not in the diff, the API returns an error — log it and continue with the next comment.
- On HTTP 403 `insufficient_scope`, the script stops immediately and prints a fix instruction. Do not retry.
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Session Persistence

Medium
Category
Rogue Agent
Content
try:
        info = api_get(host, creds["token"], "/personal_access_tokens/self")
        scopes = info.get("scopes", [])
        can_write = "api" in scopes
        if not can_write:
            print(
                "WARNING: Token scope is read-only (scopes: "
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Static analysis

No suspicious patterns detected.