Back to skill

Security audit

Pylinter Assist

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent PR linting assistant, but it can expose detected secrets in reports/comments and has unsafe file/archive handling that deserves careful review before installation.

Install only if you are comfortable auditing or patching it first. Avoid enabling PR comments or external notifications until secret findings are redacted, do not run it on untrusted diff files, and avoid downloading/extracting workflow artifacts from repositories you do not trust.

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
pylinter_assist/checks/secrets.py:88
Finding

Sensitive values are reproduced in reports and posted to GitHub

Content
View full analysis

Vulnerability Details

File Location: pylinter_assist/checks/secrets.py:88-99, pylinter_assist/reporter.py:132-135, pylinter_assist/cli.py:51-52, 144-153
Vulnerability Type: Sensitive data exposure through diagnostic output
Risk Level: High

Vulnerable Code

pylinter_assist/checks/secrets.py:88-99:

python
results.append(
    CheckResult(
        file=file_path,
        line=lineno,
        col=match.start() + 1,
        severity=Severity.ERROR,
        code=code,
        message=f"{message}: {matched_text!r}",
        check_name=self.name,
        context=stripped[:120],
    )
)

pylinter_assist/reporter.py:132-135:

python
for r in sorted(bucket, key=lambda x: (x.file, x.line)):
    file_link = f"`{r.file}:{r.line}`"
    msg = r.message.replace("|", "\\|")
    lines.append(f"| {file_link} | {r.line} | `{r.code}` | {msg} |")

pylinter_assist/cli.py:51-52:

python
if cfg["github"].get("post_comment") and token and repo:
    _post_github_comment(repo, pr_number, token, output)

pylinter_assist/cli.py:144-153:

python
def _post_github_comment(repo: str, pr_number: int, token: str, body: str):
    import requests  # noqa: PLC0415

    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/vnd.github.v3+json",
    }
    payload = {"body": body}
    resp = requests.post(url, json=payload, headers=headers, timeout=30)

Technical Analysis

The hardcoded-secret detector includes the complete regex match in CheckResult.message. For HCS001, HCS002, and HCS005 findings, this may contain plaintext passwords, credential-bearing URLs, API tokens, or cloud access keys.

The report renderer copies the message without redacting the sensitive substring. In JSON output, both message and the first 120 characters of `co ...[truncated 1726 chars]

Remediation
View remediation

Remediation Suggestions

  1. Never include matched_text or the complete source line in a secret finding.
  2. Replace the message with a fixed description containing only the finding type, filename, line, and column.
  3. If limited identification is necessary, mask nearly the entire value, for example AKIA…REDACTED, and avoid retaining enough characters to make the credential usable.
  4. Clear or redact the context field for secret-related findings.
  5. Apply centralized redaction in every renderer as a defense-in-depth control.
  6. Make remote comment posting opt-in rather than enabled by default.
  7. Add tests using representative passwords, tokens, URLs, and cloud keys. Assert that the original values never appear in text, Markdown, JSON, GitHub payloads, logs, or notification payloads.
  8. Document that any credentials previously posted by affected versions should be revoked and rotated.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
pylinter_assist/linter.py:170
Finding

Crafted unified-diff paths can read files outside the project root

Content
View full analysis

Vulnerability Details

File Location: pylinter_assist/linter.py:134-139, 170-179
Vulnerability Type: Path traversal and unauthorized local-file access
Risk Level: Medium

Vulnerable Code

pylinter_assist/linter.py:134-139:

python
for file_path in files:
    try:
        source = Path(file_path).read_text(encoding="utf-8", errors="replace")
    except OSError as exc:
        report.errors.append(f"Cannot read {file_path}: {exc}")
        continue

pylinter_assist/linter.py:170-179:

python
def _files_from_diff(diff_text: str, base_dir: str) -> list[str]:
    """Extract modified file paths from a unified diff."""
    files: list[str] = []
    for line in diff_text.splitlines():
        if line.startswith("+++ b/"):
            rel = line[6:].strip()
            full = str(Path(base_dir) / rel)
            if full not in files:
                files.append(full)
    return files

Technical Analysis

_files_from_diff treats text following +++ b/ as a trusted relative path. It joins that text to base_dir but does not reject absolute paths, parent-directory traversal, symbolic-link escapes, or candidates whose resolved location is outside the project root.

Joining a path does not provide containment. For example, with base_dir=".", a diff path such as ../../sensitive-file resolves outside the current repository. lint_files subsequently reads the resulting path using the process's existing filesystem privileges.

The issue is directly reachable through the declared lint-pr diff FILE function, where the diff file may contain arbitrary text.

Attack Path

  1. An attacker creates a unified diff containing a header such as:

    diff
    --- a/placeholder
    +++ b/../../sensitive-file
    
  2. The attacker persuades a user or automation process to run lint-pr diff crafted.patch from a directory near a sensitive readable file.

  3. `_fi ...[truncated 869 chars]

Remediation
View remediation

Remediation Suggestions

  1. Resolve the project root before processing paths:

    python
    root = Path(base_dir).resolve()
    candidate = (root / rel).resolve()
    
  2. Reject absolute paths and paths containing prohibited traversal components.

  3. Verify resolved containment with candidate.is_relative_to(root) on supported Python versions.

  4. Account for symbolic links by performing the check on fully resolved paths.

  5. Permit only expected source-file types where practical.

  6. Reject NUL characters and malformed diff paths.

  7. Apply the same containment policy to staged, explicit-file, and directory-recursion modes when the intended security boundary is the repository root.

  8. Add tests for ../, deeply nested traversal, absolute paths, symbolic-link escapes, duplicate paths, and valid in-repository paths.

T09 · Insecure Skill Coding Practices

Warning
Location
pylinter_assist/linter.py:55
Finding

Untrusted filenames can be interpreted as Pylint command-line options

Content
View full analysis

Vulnerability Details

File Location: pylinter_assist/linter.py:55-68
Vulnerability Type: Command argument injection
Risk Level: Medium

Vulnerable Code

python
cmd = [sys.executable, "-m", "pylint", "--output-format=json"]

max_line = pylint_cfg.get("max_line_length", 120)
cmd += [f"--max-line-length={max_line}"]

for msg_id in pylint_cfg.get("disable", []):
    cmd += [f"--disable={msg_id}"]
for msg_id in pylint_cfg.get("enable", []):
    cmd += [f"--enable={msg_id}"]

cmd += files

try:
    proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)

Technical Analysis

The subprocess is invoked without shell=True, which prevents shell metacharacter injection. However, filenames are appended directly to Pylint's argument list without an end-of-options delimiter.

A filename beginning with - or -- may therefore be interpreted by Pylint as an option rather than as a file operand. Such a name can originate from an explicit path or from a path parsed out of a supplied diff.

Depending on the installed Pylint version and accepted options, argument injection can alter enabled checks, select configuration, load available plugins, or otherwise change lint execution. The project permits any Pylint version at or above version 3.0, so the exact option surface is not reproducibly constrained.

Attack Path

  1. An attacker supplies a specially crafted diff or source filename whose name resembles a Pylint option.
  2. The path is collected by lint_diff, lint_files, or another linting mode.
  3. _run_pylint appends the path directly to cmd.
  4. Pylint's argument parser processes the value as an option.
  5. The injected option modifies Pylint's configuration or behavior within the linter subprocess.

No shell is involved, so this is not conventional shell injection. Exploitation is constrained to options supported by the installed Pylint release and resources available ...[truncated 593 chars]

Remediation
View remediation

Remediation Suggestions

  1. Insert an end-of-options delimiter before file operands if supported by the invoked Pylint version:

    python
    cmd += ["--"]
    cmd += files
    
  2. Normalize filenames and reject untrusted basename components beginning with - where compatibility is uncertain.

  3. Convert validated files to canonical absolute paths after enforcing project-root containment.

  4. Restrict configuration-derived Pylint options to validated identifiers and expected scalar ranges.

  5. Pin Pylint to a reviewed version or narrow version range so its accepted option surface is predictable.

  6. Add regression tests using filenames such as --disable=all.py, --rcfile=..., and other option-like operands, verifying that they are treated only as filenames or rejected.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (40)

YARA rule 'agent_skill_credential_exfiltration_webhook': AI agent skill credential harvesting followed by webhook or external exfiltration [agent_skills]

Critical
Category
YARA Match
Confidence
85% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · SKILL.md (reported line 303)May include surrounding context.

quest**: Auto-lints all files changed in a PR

  • workflow_dispatch: Manual trigger via GitHub UI or API

Manual Trigger Examples

GitHub CLI (preferred — token is handled by gh auth, not exposed in shell):

bash
gh workflow run lint-pr.yml -f pr_number=42 -f format=markdown -f post_comment=true

REST API:

Security note: The token passed via -H "Authorization: token $GITHUB_TOKEN" must be a Personal Access Token or a fine-grained token scoped to actions:write. Never hard-code the token value in scripts; always expand it from an environment variable.

bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/lint-pr.yml/dispatches \
  -d '{"ref":"main","inputs":{"pr_number":"42","format":"markdown","post_comment":"true"}}'

Output

  • Markdown reports posted as PR comments (when enabled)
  • JSON artifacts uploaded for 14 days retention
  • Exit code 1 if errors found (when `fail_

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · CLAUDE.md (reported line 132)May include surrounding context.

To enable a new project for pylinter-assist support, follow these steps on the dev branch. Review each downloaded file before committing — GitHub Actions workflows run with repository permissions and can access secrets.

bash
cd <your-project-root>

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · SKILL.md (reported line 167)May include surrounding context.

To enable a new project for pylinter-assist support, follow these steps on the dev branch. Review each downloaded file before committing — GitHub Actions workflows run with repository permissions and can access secrets.

bash
cd <your-project-root>

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · SKILL.md (reported line 14)May include surrounding context.

This project uses standard pyenv + pip + venv — no remote install scripts required.

Install pyenv (no curl | sh):

bash
# macOS

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · SKILL.md (reported line 304)May include surrounding context.

REST API:

Security note: The token passed via -H "Authorization: token $GITHUB_TOKEN" must be a Personal Access Token or a fine-grained token scoped to actions:write. Never hard-code the token value in scripts; always expand it from an environment variable.

bash

Self-Modification

High
Category
Rogue Agent
Confidence
85% confidence
Finding

Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Content

Scanner excerpt · scripts/publish.sh (reported line 190)May include surrounding context.

sh
sed -i "s/^version = \"\(.*\)\"$/version = \"$new_version\"/" "$PROJECT_ROOT/pyproject.toml"
        print_success "Version updated in pyproject.toml"

        # Update SKILL.md if it exists — pattern matches X.Y.Z and X.Y.Z+N variants
        if [ -f "$PROJECT_ROOT/SKILL.md" ]; then
            sed -i '' "s/version: [0-9][0-9.+]*/version: $new_version/" "$PROJECT_ROOT/SKILL.md" 2>/dev/null || \
                sed -i "s/version: [0-9][0-9.+]*/version: $new_version/" "$PROJECT_ROOT/SKILL.md"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · CLAUDE.md (reported line 121)May include surrounding context.

variable. The token must be scoped to actions:write for the target repository.

bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github.v3+json" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/lint-pr.yml/dispatches \

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · README.md (reported line 217)May include surrounding context.

variable. The token must be scoped to actions:write for the target repository.

bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github.v3+json" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/lint-pr.yml/dispatches \

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The top-level description says the skill provides 'Python linting,' which implies language-specific scope. Later documentation shows React useEffect checks, indicating broader source analysis than the description claims.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · SKILL.md (reported line 75)May include surrounding context.

Step 0: Post-Installation Setup

After the skill is installed as a snapshot, create a venv and install the CLI:

bash
cd ~/.openclaw/workspace/skills/pylinter-assist

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

The description presents the skill as 'Context-aware Python linting' for PR review, yet the documented checks include RUE001 and RUE002 for React useEffect dependency analysis. That is not merely omitted detail; it conflicts with the stated language-specific scope of the skill.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 308)May include surrounding context.

hard-code the token value in scripts; always expand it from an environment variable.

bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/lint-pr.yml/dispatches \
  -d '{"ref":"main","inputs":{"pr_number":"42","format":"markdown","post_comment":"true"}}'

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · CLAUDE.md (reported line 124)May include surrounding context.

md
if token:
        headers["Authorization"] = f"token {token}"

    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.text

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · README.md (reported line 220)May include surrounding context.

md
if token:
        headers["Authorization"] = f"token {token}"

    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.text

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 310)May include surrounding context.

md
if token:
        headers["Authorization"] = f"token {token}"

    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.text

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · pylinter_assist/cli.py (reported line 138)May include surrounding context.

python
if token:
        headers["Authorization"] = f"token {token}"

    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.text

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · pylinter_assist/cli.py (reported line 147)May include surrounding context.

python
if token:
        headers["Authorization"] = f"token {token}"

    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.text

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The module presents itself as a CLI for "pylinter-assist" and earlier commands focus on linting files, diffs, and pull requests. The monitor command additionally downloads GitHub Actions artifacts and sends outbound notifications to Telegram or Discord, which are not obviously required by a linting assistant's core purpose when no manifest declares that scope.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The options --timeout and --poll-interval plus the docstring "Monitor GitHub Actions workflow runs and notify when complete" imply active waiting for workflow completion. In practice, the code immediately queries status="completed" and processes the latest completed run, with no loop, timeout handling, or polling behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The callback interface accepts secret-bearing values such as Telegram bot tokens and Discord webhook URLs directly on the command line. Command-line secrets are often exposed through shell history, process listings, CI logs, and audit tooling, increasing the risk of credential leakage and subsequent unauthorized notifications or webhook abuse.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The code downloads a remote ZIP artifact from GitHub and extracts it with zipfile.extractall() into a caller-controlled directory without validating archive member paths. A malicious artifact can contain path traversal entries such as '../' or absolute paths, allowing overwrite of arbitrary files accessible to the process.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
99% confidence
Finding

This bulk download path repeatedly fetches remote artifacts and extracts each ZIP with extractall() into local directories, again without sanitizing member names. Because artifacts originate from workflow output and may be attacker-influenced in some repository or PR contexts, this expands the attack surface to arbitrary file overwrite and potentially persistence or follow-on code execution if overwritten files are later used.

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 · pylinter_assist/linter.py (reported line 68)May include surrounding context.

python
cmd += files

    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
        return [], None

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 · pylinter_assist/linter.py (reported line 157)May include surrounding context.

python
def lint_staged(self, base_dir: str = ".") -> LintReport:
        """Lint git-staged files."""
        try:
            proc = subprocess.run(
                ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
                capture_output=True, text=True, cwd=base_dir,
            )

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · pylinter_assist/notifications.py (reported line 55)May include surrounding context.

python
def __init__(self, bot_token: str, chat_id: str):
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.base_url = f"https://api.telegram.org/bot{bot_token}"

    @property
    def name(self) -> str:

Static analysis

No suspicious patterns detected.