Back to skill

Security audit

Pr Description Generator

Security checks for vulnerabilities and agentic risk

Overview

This skill is a straightforward PR-description generator, with some normal local side effects that users should invoke deliberately.

Install only if you want an agent to inspect local git repositories to generate PR text. Use explicit PR-related requests, avoid --copy or broad --repo paths for sensitive work, and provide trusted base branch names and output paths.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_pr_description.py:30
Finding

Git Option Injection Through the Unvalidated Base Revision

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_pr_description.py, lines 30-58 and 340-365
Vulnerability Type: Git argument and option injection
Risk Level: Medium

Vulnerable Code

python
def get_diff_stats(base='main', cwd=None):
    """Get file-level diff statistics."""
    output = run_git(['diff', '--stat', '--numstat', f'{base}...HEAD'], cwd=cwd)
    if not output:
        output = run_git(['diff', '--stat', '--numstat', base], cwd=cwd)
    return output


def get_diff(base='main', cwd=None):
    """Get full diff."""
    output = run_git(['diff', f'{base}...HEAD'], cwd=cwd)
    if not output:
        output = run_git(['diff', base], cwd=cwd)
    return output


def get_commits(base='main', cwd=None):
    """Get commit messages since base branch."""
    output = run_git(['log', '--oneline', '--no-merges', f'{base}...HEAD'], cwd=cwd)
    if not output:
        output = run_git(['log', '--oneline', '--no-merges', f'{base}..HEAD'], cwd=cwd)
    return output


def get_changed_files(base='main', cwd=None):
    """Get list of changed files with status."""
    output = run_git(['diff', '--name-status', f'{base}...HEAD'], cwd=cwd)
    if not output:
        output = run_git(['diff', '--name-status', base], cwd=cwd)

The value originates from the command-line argument:

python
parser.add_argument('--base', '-b', default='main',
                    help='Base branch to compare against (default: main)')

Technical Analysis

The user-controlled base value is inserted directly into several Git command argument lists without first confirming that it is a valid commit or branch name. No check rejects values beginning with -, and no safe revision-resolution step is performed.

Using an argument list with subprocess.run prevents shell metacharacter injection, but it does not prevent option injection at the Git command layer. Git may interpret a craft ...[truncated 1756 chars]

Remediation
View remediation

Remediation Suggestions

  1. Reject base values that are empty, contain control characters, or begin with -.
  2. Resolve the supplied value to a commit before using it:
    python
    def resolve_revision(base, cwd=None):
        if not base or base.startswith('-'):
            raise ValueError('Invalid base revision')
    
        result = subprocess.run(
            ['git', 'rev-parse', '--verify', f'{base}^{{commit}}'],
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )
        if result.returncode != 0:
            raise ValueError('Base revision does not resolve to a commit')
        return result.stdout.strip()
    
  3. Construct comparison ranges only from the resolved commit hash, which cannot be interpreted as a command option.
  4. Where Git syntax permits it, use explicit option terminators and unambiguous revision arguments.
  5. Modify run_git() to return and inspect the process return code rather than treating empty standard output as the only indication of failure.
  6. Add regression tests using values such as --output=..., --no-index, invalid revisions, and revision names containing unusual characters.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/generate_pr_description.py:108
Finding

Untrusted Commit Metadata Is Embedded in Markdown Without Escaping

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_pr_description.py, lines 108-126 and 251-273
Vulnerability Type: Markdown content injection
Risk Level: Low

Vulnerable Code

python
def parse_conventional_commits(commits_text):
    """Parse conventional commit messages."""
    categories = defaultdict(list)
    pattern = re.compile(r'^[a-f0-9]+\s+(feat|fix|refactor|docs|test|chore|perf|style|ci|build|revert)(?:\(([^)]+)\))?[!]?:\s*(.+)$', re.IGNORECASE)

    for line in commits_text.split('\n'):
        line = line.strip()
        if not line:
            continue
        match = pattern.match(line)
        if match:
            ctype = match.group(1).lower()
            scope = match.group(2) or ''
            msg = match.group(3).strip()
            categories[ctype].append({'scope': scope, 'message': msg})
        else:
            # Non-conventional commit
            parts = line.split(' ', 1)
            if len(parts) > 1:
                categories['other'].append({'scope': '', 'message': parts[1]})

    return categories

The extracted metadata is subsequently embedded directly into Markdown:

python
for ctype in ['feat', 'fix', 'refactor', 'perf', 'docs', 'test', 'chore', 'ci', 'build', 'revert', 'other']:
    commits = commit_categories.get(ctype)
    if commits:
        label = type_labels[ctype]
        lines.append(f'### {label}\n')
        for c in commits:
            scope = f'**{c["scope"]}**: ' if c['scope'] else ''
            lines.append(f'- {scope}{c["message"]}')
        lines.append('')

Technical Analysis

Commit subjects and conventional-commit scopes are controlled by repository contributors. The parser treats these values as data, but the Markdown generator inserts them verbatim into the resulting PR description.

A crafted commit subject can contain Markdown links, images, headings, formatting, HTML accepte ...[truncated 1779 chars]

Remediation
View remediation

Remediation Suggestions

  1. Escape Markdown metacharacters in all repository-derived values, including commit subjects and scopes.
  2. Strip control characters and normalize embedded line breaks before producing Markdown.
  3. Remove or encode raw HTML if the destination platform permits HTML in Markdown.
  4. Consider rendering untrusted commit subjects as inline code or placing them in a clearly labeled, escaped section.
  5. If external links and images are not required, neutralize link and image syntax in generated text.
  6. Treat generated repository content as untrusted when passing it to AI agents or automation; explicitly instruct downstream systems not to interpret it as commands.
  7. Add tests covering headings, links, images, HTML tags, nested formatting, control characters, and instruction-like commit subjects.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (9)

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/generate_pr_description.py (reported line 92)May include surrounding context.

python
'gemfile', 'pom.xml', 'build.gradle', 'pyproject.toml',
                              'package-lock.json', 'yarn.lock', 'poetry.lock']):
        return 'deps'
    if any(p in fp for p in ['.env', 'config/', 'settings', '.yml', '.yaml', '.toml', '.ini',
                              '.conf']):
        return 'config'
    if any(p in fp for p in ['migration', 'migrate', 'schema', '.sql']):

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/generate_pr_description.py (reported line 131)May include surrounding context.

python
'gemfile', 'pom.xml', 'build.gradle', 'pyproject.toml',
                              'package-lock.json', 'yarn.lock', 'poetry.lock']):
        return 'deps'
    if any(p in fp for p in ['.env', 'config/', 'settings', '.yml', '.yaml', '.toml', '.ini',
                              '.conf']):
        return 'config'
    if any(p in fp for p in ['migration', 'migrate', 'schema', '.sql']):

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill advertises and demonstrates shell execution and file-writing behavior via python3 scripts/generate_pr_description.py and options like --output, but it does not declare any explicit tool restrictions such as permissions or allowed-tools. In an agent environment, this can cause the skill to run with broader-than-necessary capabilities, increasing the chance of unintended command execution or writes in repositories or local paths.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The trigger list includes broad phrases such as describe changes, what changed, and PR template, which can match many ordinary requests outside the narrow intent of generating a PR description. Overbroad activation can cause the wrong skill to run, leading to unnecessary repository inspection, shell use, or file output in contexts where the user did not intend those actions.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_pr_description.py (reported line 29)May include surrounding context.

python
"""Run a git command and return stdout."""
    cmd = ['git'] + args
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd, timeout=30)
        return result.stdout.strip()
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return ''

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

Clipboard manipulation is unrelated to the core task of generating PR descriptions and introduces an additional side-effect channel. In an agent setting, silently copying repository-derived content to the system clipboard can expose sensitive code, commit messages, or secrets from diffs to other applications or subsequent user actions.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill can write generated content to any path supplied via --output, which expands it from a read/generate tool into an arbitrary file-write primitive. In agent or automated contexts, this can overwrite repository files, user dotfiles, CI artifacts, or other sensitive local paths if arguments are attacker-influenced.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_pr_description.py (reported line 385)May include surrounding context.

python
if args.copy:
        try:
            proc = subprocess.Popen(['xclip', '-selection', 'clipboard'],
                                    stdin=subprocess.PIPE)
            proc.communicate(description.encode())
        except FileNotFoundError:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_pr_description.py (reported line 390)May include surrounding context.

python
proc.communicate(description.encode())
        except FileNotFoundError:
            try:
                proc = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE)
                proc.communicate(description.encode())
            except FileNotFoundError:
                print("(clipboard copy failed — install xclip or pbcopy)", file=sys.stderr)

Static analysis

No suspicious patterns detected.