Back to skill

Security audit

GitHub Release Analyzer

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its GitHub release-summary purpose, but its cron state key can escape the intended state directory and read or overwrite other JSON files the agent can access.

Review before installing, especially if cron jobs or shared instructions can set --state-key. Use only trusted repository targets and avoid custom state keys until path validation is added. The skill should also add an explicit rule that GitHub release bodies are untrusted content and must never control agent behavior.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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

Error
Location
scripts/state_store.py:46
Finding

Unrestricted State Key Allows Path Traversal and Arbitrary JSON File Access

Content
View full analysis
Path: key = state_key or repo.state_key return DEFAULT_STATE_ROOT / f"{key}.json" def load_state(repo: RepoSpec, state_key: str | None = None) -> tuple[StateData, Path, bool]: path = state_path(repo, state_key) path.parent.mkdir(parents=True, exist_ok=True) if not path.exists(): return StateData(repo=repo.slug), path, True data = json.loads(path.read_text(encoding="utf-8")) state = StateData( repo=data.get("repo", repo.slug), processed_tags=list(data.get("processed_tags", [])), latest_processed_release_id=data.get("latest_processed_release_id"), latest_processed_published_at=data.get("latest_processed_published_at"), last_checked_at=data.get("last_checked_at"), last_success_at=data.get("last_success_at"), initialized_at=data.get("initialized_at"), ) is_first_run = not bool(state.initialized_at) return state, path, is_first_run def save_state(path: Path, state: StateData) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(state.to_dict(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8") ``` ### Technical Analysis The `--state-key` command-line value is inserted directly into a filesystem path without validation, normalization, or a containment check. A state key containing parent-directory components such as `../` can cause the resulting path to escape `DEFAULT_STATE_ROOT`. Both cron preparation and commit operations call `load_ ...[truncated 2205 chars]
Remediation
View remediation
str: if not STATE_KEY_PATTERN.fullmatch(key): raise ValueError("invalid state key") if key in {".", ".."}: raise ValueError("invalid state key") return key ``` 2. Resolve the state root and candidate path, then enforce containment: ```python def state_path(repo: RepoSpec, state_key: str | None = None) -> Path: key = validate_state_key(state_key or repo.state_key) root = DEFAULT_STATE_ROOT.resolve() candidate = (root / f"{key}.json").resolve() if candidate.parent != root: raise ValueError("state path escapes the configured state root") return candidate ``` 3. Explicitly reject `/`, `\`, absolute paths, drive prefixes, null bytes, and parent-directory components for cross-platform safety. 4. Protect against symbolic-link attacks. Refuse state paths that are symlinks and, where supported, use no-follow file operations. 5. Write state atomically through a securely created temporary file in the same directory, flush and synchronize it as appropriate, and replace the destination only after serialization succeeds. 6. Consider setting restrictive state directory and file permissions because release state should not be writable by unrelated local users. 7. Add automated tests for: - Unix and Windows traversal sequences. - Absolute paths and drive-qualified paths. - Nested separators. - Symbolic-link targets. - Excessively long state keys. - Valid default and explicitly supplied state keys. - Verification that every resolved path remains inside the configured state root. ]]>

T01 · Skill Instruction Hijacking

Warning
Location
references/summary-contract.md:9
Finding

Untrusted GitHub Release Notes Are Passed to the Agent Without Prompt-Injection Isolation

Content
View full analysis
3. If `status=has_updates`, read `references/summary-contract.md` and produce one summary string per selected release. Keep the summary language aligned with the primary language of the invocation instruction. ``` ```markdown Each selected release produces exactly one summary string. ## Requirements - Output in the primary language of the invocation instruction (zh → 中文, en → 英文) - Keep facts anchored to the release body - Keep the summary distilled and high-signal, but do not force it into a fixed length budget - Output JSON only. Each summary item must be a single non-empty string, not an object. - Distill the release into a readable, high-signal summary rather than mirroring the changelog. - Use markdown to make the result easy to scan. Prefer short sections, bullet lists, and compact grouping over long continuous prose. - Do not force a fixed internal structure, but the summary should still feel organized and easy to skim. - Prioritize the most important themes and expand when the release has multiple genuinely me ...[truncated 3015 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared purpose is about analyzing GitHub repository releases and summarizing release notes. The supplied code does not perform release fetching, parsing, summarization, GitHub API interaction, cron tracking, or release-note analysis. Instead, it is an internal test suite for platform compatibility, command usage, metadata presence, and state storage path handling. These behaviors are materially different from the skill’s declared end-user function, so this chunk does not accurately represent the declared purpose.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
the directory containing the loaded `SKILL.md` (the Skill root). Do not run

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill instructs the agent to run local Python scripts, read reference files, potentially access the network for GitHub data, and write persistent state in cron mode, yet it declares no explicit tool scope or permission boundaries. This creates an over-privileged execution surface where the runtime may permit broader file, shell, or network actions than users expect, increasing the blast radius if the skill or its scripts are modified or abused.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

In cron mode, the skill persists processed tags and latest release metadata after delivery, but the user-facing contract does not prominently disclose that persistent state will be written. Hidden state mutation can surprise operators, complicate auditing, and be abused to suppress future notifications or alter monitoring behavior if state is tampered with.

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 · scripts/models.py (reported line 22)May include surrounding context.

python
@property
    def api_url(self) -> str:
        return f"https://api.github.com/repos/{self.owner}/{self.repo}/releases"


@dataclass

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The code sets the output language to "zh" whenever neither language nor outputLanguage is provided. This imposes a specific locale by default and does not offer an explicit user choice or opt-in, which matches the language/locale policy violation criteria.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The file hard-codes supported languages to only "zh" and "en", and later defaults to "zh" when no language is provided. Under the policy, forcing a specific language or limiting locale choice without opt-in or a documented region-specific justification is a natural-language policy concern.

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 · tests/test_platform_compat.py (reported line 34)May include surrounding context.

python
self.assertNotIn("python3 skills/github-release-analyzer/scripts/run.py", skill_md)

    def test_relative_script_command_runs_from_skill_root(self) -> None:
        result = subprocess.run(
            [sys.executable, "scripts/run.py", "--help"],
            cwd=REPO_ROOT,
            capture_output=True,

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

Line L16 says the output must be JSON only and that each summary item must be a single string, while L18 requires using Markdown to format the result. For a skill intended to emit plain summary strings inside JSON, this creates active documentation conflict about whether the final output is raw JSON strings or Markdown-rendered content.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

This code performs an HTTP request to an external service using urllib.request.urlopen, which transmits repository URL metadata and receives remote content. Within this file there is no confirmation prompt, user-facing log/print, or comment/docstring disclosing that outbound network access occurs.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

The inline comment is written in Chinese while the rest of the file is in English, introducing a language-specific instruction without any stated user choice or documented locale requirement. This can violate language/locale policy expectations when a skill otherwise appears language-neutral.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The validation logic raises an error for any language outside the fixed supported set, which enforces the locale restriction rather than providing an opt-in choice. This can violate the language/locale policy unless the restriction is clearly documented and justified elsewhere.

Content

No source excerpt is available for this finding.