Back to skill

Security audit

GitHub Semantic Search

Security checks for vulnerabilities and agentic risk

Overview

This GitHub assistant is mostly purpose-aligned, but it has under-disclosed high-impact behavior around external Feishu alerts, authenticated GitHub CLI use, and destructive local index operations.

Install only if you are comfortable letting the skill query GitHub through your authenticated gh session, store issue and PR text in a local Qdrant collection, send text to local Ollama for embeddings, and potentially forward repository activity to Feishu. Before use, remove the hard-coded Feishu recipient, add explicit repo validation, change subprocess calls to argument lists, and avoid running init on an existing collection unless you intend to delete and recreate it.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (4)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/github_indexer.py:28
Finding
GitHub CLI Argument Injection in Repository Indexing<![CDATA[ ## Vulnerability Details **File Location**: `scripts/github_indexer.py:28-32`; user-controlled values reach the helper at `scripts/github_indexer.py:153`, `179`, and `211` **Vulnerability Type**: Improper subprocess argument construction **Risk Level**: Medium ### Vulnerable Code ```python def gh(args: str) -> dict: result = subprocess.run( f'"{GH_EXE}" {args}', capture_output=True, encoding="utf-8", errors="replace" ) ``` Representative user-controlled call: ```python def fetch_issues(repo: str, state: str = "all", limit: int = 100) -> List[GitHubItem]: data = gh_list(f"issue list --repo {repo} --state {state} --limit {limit} --json number,title,body,state,author,labels,url,createdAt,updatedAt") ``` Related call sites construct commands in the same manner: ```python data = gh_list(f"pr list --repo {repo} --state {state} --limit {limit} --json number,title,body,state,author,labels,url,createdAt,updatedAt,isDraft") ``` ```python data = gh(f"repo view {repo} --json name,description,stargazerCount,url,languages,repositoryTopics") ``` ### Technical Analysis The repository name and other arguments are interpolated into a single command-line string. On the intended Windows environment, process argument parsing can interpret spaces and quotes in an attacker-controlled repository value as argument boundaries. The input is not validated as an `owner/repository` identifier. Because `shell=False` is used, this is not a confirmed shell-metacharacter or arbitrary operating-system command injection vulnerability. It is an argument-injection weakness: crafted input may introduce additional GitHub CLI flags or otherwise alter the intended request. The command executes under the operator's existing authenticated GitHub CLI identity. ### Attack Path 1. An attacker able to invoke the script supplies a crafted value for the positional `repo` argument. 2. The value passes through `argparse` without repository-format validation. 3. ` ...[truncated 705 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Change subprocess helpers to accept `list[str]` rather than preformatted strings. - Pass every argument as a separate list element: ```python result = subprocess.run( [GH_EXE, "issue", "list", "--repo", repo, "--state", state, "--limit", str(limit), "--json", "number,title,body,state,author,labels,url,createdAt,updatedAt"], capture_output=True, encoding="utf-8", errors="replace", check=False, ) ``` - Validate repository identifiers before invocation, for example with a strict pattern such as: ```python r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" ``` - Reject control characters, whitespace, quotes, and values beginning with an option prefix. - Apply reasonable upper and lower bounds to numeric arguments such as `limit`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/github_search.py:103
Finding
GitHub CLI Argument Injection in CI Status Lookup<![CDATA[ ## Vulnerability Details **File Location**: `scripts/github_search.py:103-107` **Vulnerability Type**: Improper subprocess argument construction **Risk Level**: Medium ### Vulnerable Code ```python def get_ci_status(repo: str, limit: int = 5) -> str: try: result = subprocess.run( f'"{GH_EXE}" run list --repo {repo} --limit {limit} --json name,status,conclusion,createdAt,headBranch,url', capture_output=True, encoding="utf-8", errors="replace" ) ``` The unvalidated value originates from the command line: ```python parser.add_argument("--repo", help="Filter by repo (owner/repo)") ``` It reaches the subprocess when CI output is requested: ```python if args.ci and args.repo: print(get_ci_status(args.repo, limit=5)) ``` ### Technical Analysis The user-supplied repository value is inserted into a single command-line string. In the intended Windows environment, spaces or quotes in this value can affect argument boundaries and introduce unintended GitHub CLI options. The use of `shell=False` limits this to argument injection rather than confirmed arbitrary shell-command execution. Nevertheless, GitHub CLI still runs with the operator's authenticated credentials and may accept injected options that alter the intended API request. ### Attack Path 1. An attacker or untrusted caller invokes the search command with `--ci` and a crafted `--repo` value. 2. The repository value is accepted without syntax validation. 3. `get_ci_status()` concatenates it into the `gh run list` command string. 4. Process argument parsing may treat portions of the value as additional CLI arguments. 5. GitHub CLI performs the modified CI lookup with the operator's authenticated permissions. ### Impact Assessment A successful attack may cause CI information to be requested from an unintended repository or change supported listing behavior. For accounts with access to private repositories, this may expose workflow names, branches, c ...[truncated 182 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Invoke GitHub CLI with an explicit argument list: ```python result = subprocess.run( [ GH_EXE, "run", "list", "--repo", repo, "--limit", str(limit), "--json", "name,status,conclusion,createdAt,headBranch,url", ], capture_output=True, encoding="utf-8", errors="replace", ) ``` - Validate `repo` against a strict `owner/repository` format before invoking GitHub CLI. - Reject whitespace, quotes, control characters, and option-like values. - Bound `limit` to a small positive range. - Use centralized, typed wrappers for each allowed GitHub CLI operation rather than accepting arbitrary command strings. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/github_monitor.py:51
Finding
Unvalidated Monitor State Enables GitHub CLI Argument Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/github_monitor.py:51-64`; affected command construction occurs at `scripts/github_monitor.py:84`, `90`, and `96` **Vulnerability Type**: Improper subprocess argument construction and unvalidated persistent state **Risk Level**: Medium ### Vulnerable Code ```python def gh(args: str) -> dict: result = subprocess.run( f'"{GH_EXE}" {args}', capture_output=True, text=True, shell=False ) if result.returncode != 0: return {} try: return json.loads(result.stdout) if result.stdout.strip() else {} except: return {} def gh_list(args: str) -> list: result = subprocess.run( f'"{GH_EXE}" {args}', capture_output=True, text=True, shell=False ) ``` Repository values loaded from monitor state reach the command builder: ```python def check_new_issues(repo: str, since: str) -> list: """Find issues created after `since`.""" items = gh_list(f"issue list --repo {repo} --state open --limit 50 --json number,title,body,createdAt,labels,url,author") return [i for i in items if i.get("createdAt", "") > since] def check_new_prs(repo: str, since: str) -> list: """Find PRs created after `since`.""" items = gh_list(f"pr list --repo {repo} --state open --limit 50 --json number,title,body,createdAt,labels,url,author,isDraft") return [i for i in items if i.get("createdAt", "") > since] def check_ci_failures(repo: str, since: str) -> list: """Find failed CI runs after `since`.""" items = gh_list(f"run list --repo {repo} --limit 30 --json id,name,status,conclusion,createdAt,headBranch,url") ``` ### Technical Analysis Repository values are stored in a writable JSON state file and restored without schema or syntax validation. The restored value is concatenated into GitHub CLI command strings. On the intended Windows platform, crafted whitespace or quoting can affect argument parsing and add unintended CLI option ...[truncated 1455 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace generic string-based helpers with operation-specific list-based invocations: ```python result = subprocess.run( [ GH_EXE, "issue", "list", "--repo", repo, "--state", "open", "--limit", "50", "--json", "number,title,body,createdAt,labels,url,author", ], capture_output=True, text=True, shell=False, ) ``` - Validate repository values both when saving and when loading state. - Enforce a schema for every state record, including allowed event values, keyword types, and timestamp format. - Reject malformed state rather than silently executing with partially trusted values. - Store state with permissions limited to the current user. - Consider integrity protection if another lower-trust process can modify the state file. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/github_monitor.py:73
Finding
Hard-Coded Feishu Recipient Can Receive Repository Activity<![CDATA[ ## Vulnerability Details **File Location**: `scripts/github_monitor.py:73-79` **Vulnerability Type**: Hard-coded external message destination **Risk Level**: Medium ### Vulnerable Code ```python def send_feishu(message: str, user_id: str = "ou_4bf3393b288ddc97e3bfe1759bf99f43"): """Send alert to Feishu via openclaw.""" result = subprocess.run( f'openclaw message send --channel feishu --to user:{user_id} --message {json.dumps(message)}', capture_output=True, text=True, shell=False ) return result.returncode == 0 ``` The intended transmission path is: ```python if high_priority and args.feishu: msg = f"**🚨 GitHub Alert — {len(high_priority)} high priority event(s)**\n\n" msg += "\n\n".join(format_alert(a) for a in high_priority) send_feishu(msg) print("\n📨 High priority alert sent to Feishu.") ``` ### Technical Analysis The Feishu recipient is embedded directly in source code. A caller that invokes `send_feishu()` without overriding `user_id` sends alerts to that fixed account, regardless of who installed or operates the Skill. Alert messages can contain repository names, issue or pull-request titles, authors, labels, URLs, CI workflow names, and branch names. These fields may disclose private project activity. The normal command-line path is currently defective because `cmd_check()` accesses `args.feishu`, but the parser does not register a `--feishu` option. Therefore, ordinary `check` execution does not currently complete this transmission path. The function remains directly callable, and merely fixing the missing parser option would activate the hard-coded destination unless the destination design is also corrected. ### Attack Path 1. The monitor reads repository activity using the operator's authenticated GitHub CLI account. 2. An issue, pull request, or failed CI run is classified as high priority. 3. The alert is formatted with repository metadata. 4. `send_feishu()` is invoked directly, ...[truncated 667 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the hard-coded default recipient and require an explicit destination: ```python def send_feishu(message: str, user_id: str): if not user_id: raise ValueError("A Feishu recipient must be explicitly configured") ``` - Store the recipient in protected user configuration or accept it through a clearly documented command-line option. - Display the configured destination and require confirmation before the first transmission. - Document exactly which repository fields are transmitted externally. - Provide a redaction mode for private repository names, branches, authors, and URLs. - Register and validate the intended `--feishu` option, or remove the unreachable feature entirely. - Invoke OpenClaw with an argument list instead of a concatenated string: ```python subprocess.run( [ "openclaw", "message", "send", "--channel", "feishu", "--to", f"user:{user_id}", "--message", message, ], capture_output=True, text=True, shell=False, ) ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (27)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
A second description-behavior mismatch indicates the skill markets an Embedder+Qdrant+LLM assistant while the underlying behavior is narrower and different. Overstated AI and indexing claims can mislead users into granting trust and permissions under false assumptions, which is especially risky for a GitHub-integrated skill handling repository metadata and workflow context.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
A second description-behavior mismatch indicates the skill markets an Embedder+Qdrant+LLM assistant while the underlying behavior is narrower and different. Overstated AI and indexing claims can mislead users into granting trust and permissions under false assumptions, which is especially risky for a GitHub-integrated skill handling repository metadata and workflow context.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
python github_indexer.py add owner/repo --repo        # Index repo metadata
  python github_indexer.py add owner/repo --all         # Index all
  python github_indexer.py status                      # Show indexed repos
  python github_indexer.py rm owner/repo               # Remove repo from index
"""
import argparse
import json
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
python github_indexer.py add owner/repo --repo        # Index repo metadata
  python github_indexer.py add owner/repo --all         # Index all
  python github_indexer.py status                      # Show indexed repos
  python github_indexer.py rm owner/repo               # Remove repo from index
"""
import argparse
import json
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Missing User Warnings

High
Confidence
97% confidence
Finding
The `init` command always calls `ensure_collection(qc, force=True)`, which deletes any existing Qdrant collection before recreating it. This creates a destructive operation with no confirmation, backup, or safety check, making accidental or automated data loss likely in an agent-driven environment.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill advertises shell, network, file read/write, and environment-capable behavior but does not declare any explicit tool scope or permission boundaries. In an agent setting, this increases the chance of overbroad execution, data access, or unintended side effects because the operator cannot tell what capabilities are intended or restricted.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The monitoring feature mentions Feishu alerts but does not warn that repository activity or matched content may be transmitted to an external messaging platform. This can leak confidential project information, issue titles, CI failures, or keywords to third-party systems and audiences beyond GitHub's original access model.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The monitoring feature mentions Feishu alerts but does not warn that repository activity or matched content may be transmitted to an external messaging platform. This can leak confidential project information, issue titles, CI failures, or keywords to third-party systems and audiences beyond GitHub's original access model.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def gh(args: str) -> dict:
    result = subprocess.run(
        f'"{GH_EXE}" {args}',
        capture_output=True, encoding="utf-8", errors="replace"
    )
Confidence
97% confidence
Finding
The code constructs a command string with untrusted input (`args`) and passes it to `subprocess.run` without an argument list. Because user-controlled repository names are interpolated into CLI arguments such as `--repo {repo}`, this creates a command-injection risk if shell parsing is used by the platform/runtime; at minimum it is unsafe command construction around external process execution.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def gh_list(args: str) -> list:
    result = subprocess.run(
        f'"{GH_EXE}" {args}',
        capture_output=True, encoding="utf-8", errors="replace"
    )
Confidence
97% confidence
Finding
This repeats the same unsafe pattern in `gh_list`: a formatted command string includes untrusted arguments and is sent to `subprocess.run`. In a GitHub assistant context where repo identifiers may originate from user prompts or other automation, this increases the chance of attacker-controlled input reaching process execution.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The indexer sends issue, PR, and repository text to a local embedding service with no consent, classification, or redaction step. Even though the endpoint is localhost, this still exports potentially sensitive repository content to another service boundary, which may log, retain, or further process private data.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The `rm` command deletes all indexed items for a repository immediately with no confirmation or dry-run mode. In an assistant skill, where commands may be triggered from natural language workflows, lack of friction on destructive actions increases the chance of accidental deletion or abuse.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def gh(args: str) -> dict:
    result = subprocess.run(
        f'"{GH_EXE}" {args}',
        capture_output=True, text=True, shell=False
    )
Confidence
94% confidence
Finding
The code constructs a single command string from a caller-controlled `args` value and passes it to `subprocess.run` with `shell=False`. On Python, supplying a string with `shell=False` still delegates parsing to the target process/runtime rather than safely separating arguments, so untrusted repo names or other fields embedded into `args` can become argument-injection into `gh` (e.g. adding extra flags or changing behavior). In this skill, repository identifiers come from user input and persisted state, which makes the monitor context materially increase exploitability.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def gh_list(args: str) -> list:
    result = subprocess.run(
        f'"{GH_EXE}" {args}',
        capture_output=True, text=True, shell=False
    )
Confidence
94% confidence
Finding
This has the same unsafe pattern as `gh()`: a single formatted command string is built from untrusted `args` and executed via `subprocess.run`. Because `args` is derived from values like `repo` that originate from CLI input or local state, an attacker can inject additional `gh` options or alter which repository/data is queried, leading to unintended command execution semantics and possible data exposure.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def send_feishu(message: str, user_id: str = "ou_4bf3393b288ddc97e3bfe1759bf99f43"):
    """Send alert to Feishu via openclaw."""
    result = subprocess.run(
        f'openclaw message send --channel feishu --to user:{user_id} --message {json.dumps(message)}',
        capture_output=True, text=True, shell=False
    )
Confidence
87% confidence
Finding
The Feishu sender also builds a single command string containing user-controlled `message` and potentially `user_id`, then executes it with `subprocess.run`. Even though `json.dumps` reduces some quoting issues, this still relies on downstream parsing of a flat command string and can permit argument confusion or malformed delivery; additionally, it transmits potentially sensitive repository content to an external messaging channel.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The docstring says this function will 'Format alert as readable text', implying it can format the alerts produced by the monitor. However, CI failure alerts created in L165-L174 do not include 'number' or 'title', while the formatter unconditionally accesses alert['number'] and alert['title'] on L188, contradicting the documented intent for this formatter in the context of all alert types used by the script.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
High-priority alerts are automatically sent to Feishu and include issue/PR titles, labels, URLs, workflow names, and potentially body-derived keyword context without an explicit confirmation step at execution time. In a GitHub monitoring skill, repository content may be private or sensitive, so automatic forwarding to an external messaging platform increases data-leak risk, especially if users do not realize the check path exfiltrates data.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The function posts the user's natural-language query to an HTTP service at localhost:11434 to generate embeddings. Although the behavior is part of semantic search, there is no explicit warning in the docstring or inline comments that user-supplied text is transmitted to another service.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Content
def get_embedding(text: str) -> list:
    payload = {"model": EMBED_MODEL, "prompt": text}
    req = urllib.request.Request(
        "http://localhost:11434/api/embeddings",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
Confidence
70% confidence
Finding
Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Content
def get_embedding(text: str) -> list:
    payload = {"model": EMBED_MODEL, "prompt": text}
    req = urllib.request.Request(
        "http://localhost:11434/api/embeddings",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
Confidence
70% confidence
Finding
Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The helper executes the GitHub CLI via subprocess, which is a safety-relevant operation for code files under this rule. While the script has a top-level docstring describing search usage, it does not disclose that it will invoke an external executable, and this helper itself has no confirmation prompt, comment, or warning.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def gh(args: str) -> dict:
    result = subprocess.run(
        f'"{GH_EXE}" {args}',
        capture_output=True, encoding="utf-8", errors="replace"
    )
Confidence
95% confidence
Finding
The gh(args) helper builds a command string by concatenating arbitrary args into subprocess.run without argument separation or validation. If this helper is ever called with untrusted input, it can enable command/argument injection or unintended GitHub CLI operations, and the function is broader than needed because it allows effectively arbitrary gh subcommands.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The file is documented and presented as a semantic search tool for indexed GitHub data, but it also invokes GitHub CLI to fetch live workflow run status when --ci is used. While repo monitoring is mentioned in the broader skill manifest, this specific script's stated purpose and docstring are limited to natural-language search, so the added live CI inspection behavior exceeds the described operation of this file.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
When --ci is used, the script runs an external GitHub CLI command to query workflow runs for a repository. This is a subprocess-based operation and the file lacks any warning or explanatory comment disclosing that the flag triggers external command execution and data retrieval.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def get_ci_status(repo: str, limit: int = 5) -> str:
    try:
        result = subprocess.run(
            f'"{GH_EXE}" run list --repo {repo} --limit {limit} --json name,status,conclusion,createdAt,headBranch,url',
            capture_output=True, encoding="utf-8", errors="replace"
        )
Confidence
90% confidence
Finding
The CI status command interpolates repo directly into a subprocess command string. Although argparse constrains typical usage, using a string command instead of an argv list still creates unnecessary injection risk and can cause unintended gh behavior if repo is attacker-controlled in another invocation context.

Static analysis

No suspicious patterns detected.