Back to skill

Security audit

ZeeLin-report-publisher

Security checks for vulnerabilities and agentic risk

Overview

The skill is a mostly coherent report publisher, but it also performs persistent GitHub and SSH account setup and has a path-boundary bug that can write outside the target repository.

Install only if you are comfortable with an agent publishing to GitHub and changing persistent local/account state. Prefer running bootstrap with --skip-ssh and --skip-gh-login unless you intentionally want it to manage GitHub authentication, review --config-path carefully, use --dry-run first, and confirm the target repo/remotes before allowing push or PR creation.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish_report.py:376
Finding
Repository Path Escape Allows Modification of Files Outside the Target Repository<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish_report.py`, lines 376–377 and 416–497 **Vulnerability Type**: Insufficient path-boundary validation **Risk Level**: High ### Vulnerable Code ```python repo_dir = Path(args.repo).resolve() source_file = Path(args.report_file).expanduser().resolve() config_path = (repo_dir / args.config_path).resolve() public_dir = config_path.parent ``` The resolved path is subsequently read and used to derive the report destination: ```python records = load_reports_config(config_path) existing_ids = {str(item.get("id", "")).strip() for item in records if isinstance(item, dict)} category_dir_name = sanitize_path_segment(args.category_dir or args.category) dest_dir = public_dir / category_dir_name target_path = pick_unique_destination(dest_dir, source_file, overwrite=args.overwrite) rel_asset = target_path.relative_to(public_dir).as_posix() ``` Both the selected JSON file and the derived destination are modified before repository-relative validation occurs: ```python dest_dir.mkdir(parents=True, exist_ok=True) if not target_path.exists() or file_sha256(target_path) != file_sha256(source_file): shutil.copy2(source_file, target_path) records.insert(0, entry) write_reports_config(config_path, records) if not args.skip_build: print("[INFO] Running npm run build ...") run_cmd(["npm", "run", "build"], cwd=repo_dir) # ... rel_config = config_path.relative_to(repo_dir).as_posix() rel_target = target_path.relative_to(repo_dir).as_posix() ``` ### Technical Analysis The `--config-path` argument is documented as repository-relative, but the implementation accepts absolute paths and traversal sequences such as `../../target.json`. Calling `resolve()` normalizes the path but does not ensure that the resulting path remains under `repo_dir`. The parent directory of the escaped configuration path becomes `public_dir`. Consequently, `dest_dir` and `target_path` can also point outside the repository. The ...[truncated 2068 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject escaped configuration paths immediately after resolution and before reading or writing anything: ```python repo_dir = Path(args.repo).resolve() config_path = (repo_dir / args.config_path).resolve() if not config_path.is_relative_to(repo_dir): raise PublishError("--config-path must remain inside the repository.") ``` 2. Prefer removing the configurable path entirely and always use the declared fixed location: ```python config_path = repo_dir / "public" / "reports_config.json" ``` 3. Independently validate all derived paths before filesystem mutation: ```python public_dir = config_path.parent.resolve() dest_dir = (public_dir / category_dir_name).resolve() target_path = (dest_dir / source_file.name).resolve() for path in (public_dir, dest_dir, target_path): if not path.is_relative_to(repo_dir): raise PublishError(f"Path escapes repository: {path}") ``` 4. Perform every path and branch precondition check before copying the report or rewriting the configuration. 5. Write the updated JSON to a temporary file inside the validated repository and atomically replace the original only after validation succeeds. 6. If later operations such as the build or Git branch creation fail, restore the original configuration and remove newly copied files, or use a temporary worktree so publication is transactional. 7. Add tests covering absolute paths, `..` traversal, symlink-based escapes, and escaped paths in both normal and dry-run modes. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/bootstrap_github.sh:117
Finding
Bootstrap Performs Account-Wide Git and SSH Credential Provisioning by Default<![CDATA[ ## Vulnerability Details **File Location**: `scripts/bootstrap_github.sh`, lines 117–118 and 147–174 **Vulnerability Type**: Excessive credential and configuration scope **Risk Level**: Medium ### Vulnerable Code The bootstrap changes Git identity globally rather than only for the report repository: ```bash git config --global user.name "$GIT_NAME" git config --global user.email "$GIT_EMAIL" info "Configured git identity: $GIT_NAME <$GIT_EMAIL>" ``` Unless the caller explicitly supplies `--skip-ssh`, it creates a default, unencrypted SSH identity and may upload its public key to the authenticated GitHub account: ```bash if [[ $SKIP_SSH -eq 0 ]]; then need_cmd ssh need_cmd ssh-keygen KEY_PATH="${HOME}/.ssh/id_ed25519" PUB_PATH="${KEY_PATH}.pub" if [[ ! -f "$KEY_PATH" ]]; then info "Generating SSH key: $KEY_PATH" mkdir -p "${HOME}/.ssh" chmod 700 "${HOME}/.ssh" ssh-keygen -t ed25519 -C "$GIT_EMAIL" -f "$KEY_PATH" -N "" else info "Existing SSH key found: $KEY_PATH" fi if [[ ! -f "$PUB_PATH" ]]; then die "Missing SSH public key: $PUB_PATH" fi if [[ $HAS_GH -eq 1 && $GH_AUTH_OK -eq 1 ]]; then PUB_KEY="$(cat "$PUB_PATH")" REMOTE_KEYS="$(gh api user/keys --jq '.[].key' 2>/dev/null || true)" if grep -Fxq "$PUB_KEY" <<< "$REMOTE_KEYS"; then info "SSH key already uploaded to GitHub." else KEY_TITLE="$(hostname)-$(date +%F)" info "Uploading SSH key to GitHub as: $KEY_TITLE" gh ssh-key add "$PUB_PATH" --title "$KEY_TITLE" fi else warn "Skip SSH key upload because gh is unavailable or unauthenticated." fi fi ``` ### Technical Analysis The declared workflow requires enough authorization to push a feature branch and create a pull request. It does not inherently require: - Replacing the user's global Git identity. - Creating the user's default SSH identity. - Creating a private key without a passphrase. - Registering that identity across the authenticated GitHub ...[truncated 2674 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make SSH-key creation and upload opt-in rather than default, for example through separate `--create-ssh-key` and `--upload-ssh-key` flags. 2. Require explicit confirmation immediately before: - Generating a private key. - Uploading a public key to GitHub. - Changing global Git configuration. 3. Configure identity locally for the selected repository: ```bash git -C "$REPO_PATH" config user.name "$GIT_NAME" git -C "$REPO_PATH" config user.email "$GIT_EMAIL" ``` 4. Prefer existing GitHub CLI HTTPS authentication where available instead of requiring SSH provisioning. 5. If a new SSH key is necessary, use a dedicated path such as: ```text ~/.ssh/zeelin_report_publisher_ed25519 ``` Configure only the relevant GitHub remote or SSH host alias to use that identity. 6. Avoid `-N ""`. Prompt securely for a passphrase or use an SSH agent and clearly document the resulting security trade-offs. 7. Display the authenticated GitHub account and requested operation, then obtain confirmation before calling `gh ssh-key add`. 8. Document key ownership, intended scope, storage location, rotation, and revocation procedures. 9. Preserve existing global Git identity by default and provide a separate, clearly named flag if a user intentionally wants global configuration. 10. Keep the secure directory permissions, and additionally verify private-key permissions are restricted to the owner. ]]>
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
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose is report publishing, but the documented workflow also performs global git identity changes, GitHub authentication, SSH key generation/upload, cloning, and remote configuration. This mismatch hides materially more sensitive behavior than users would reasonably expect, which can lead to credential changes, account linkage, or repository access modifications under the guise of a content-publishing task.

Credential Access

High
Category
Privilege Escalation
Content
if [[ $SKIP_SSH -eq 0 ]]; then
  need_cmd ssh
  need_cmd ssh-keygen
  KEY_PATH="${HOME}/.ssh/id_ed25519"
  PUB_PATH="${KEY_PATH}.pub"

  if [[ ! -f "$KEY_PATH" ]]; then
Confidence
90% confidence
Finding
Referencing the user’s default ~/.ssh/id_ed25519 path is not inherently malicious, but in this script it is part of a workflow that creates, reads, and uploads authentication material tied to the user’s GitHub account. In a report-publishing skill, touching long-lived personal SSH credentials is overly invasive and increases the risk of unintended credential exposure, misuse, or account modification.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill invokes shell commands that can read/write files, access the network, and mutate git state, but it does not declare any explicit tool scope or permission boundaries. That makes the effective authority of the skill opaque to users and reviewers, increasing the chance of over-privileged execution or accidental sensitive actions.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The bootstrap instructions trigger sensitive account and credential operations, including git identity setup, GitHub auth, SSH key upload, and permission validation, without an explicit warning that user account state will be changed. In context, this is more dangerous because the skill is framed as a report publisher, so users may not anticipate persistent auth or SSH modifications.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The publish workflow copies files, edits repository configuration, creates branches, commits, pushes remotely, and may open a PR, but it does not present an explicit safety warning about local mutations and outbound network actions. That can cause unreviewed changes to be written and transmitted to remotes before the user fully understands the side effects.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The instruction 'Use one sentence in Chinese' imposes a specific language requirement. The file does not offer the user a language choice or explain why Chinese is mandatory for policy or region-specific reasons.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The script’s documented behavior includes global Git identity configuration, GitHub CLI authentication, SSH key generation/upload, and remote permission probing, which materially exceed the narrow report-publishing purpose in the skill metadata. In an agent skill context, these account-level changes expand the trust boundary and can modify a user’s global development environment and GitHub access in ways unrelated to publishing a report.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
This section generates an SSH key under the user’s home directory, attempts GitHub CLI login, enumerates existing account keys, and uploads a new SSH key to the user’s GitHub account. Those are sensitive credential-management operations with persistent account impact, and they are not necessary for merely copying report assets or updating a config file, making them especially risky in an automation skill.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if [[ ! -f "$KEY_PATH" ]]; then
    info "Generating SSH key: $KEY_PATH"
    mkdir -p "${HOME}/.ssh"
    chmod 700 "${HOME}/.ssh"
    ssh-keygen -t ed25519 -C "$GIT_EMAIL" -f "$KEY_PATH" -N ""
  else
    info "Existing SSH key found: $KEY_PATH"
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_cmd(cmd: list[str], cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
    result = subprocess.run(
        cmd,
        cwd=str(cwd),
        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The default abstract is hard-coded in Chinese, and the CLI description also targets a Chinese-language site, but the script does not offer any language selection or explicit opt-in for generated content language. This can violate language/locale policy when users expect locale-neutral behavior or need a different language.

Intent-Code Divergence

Low
Confidence
82% confidence
Finding
The usage text frames cloning and verification as optional helper actions, but the script is fundamentally a GitHub environment bootstrap tool that alters global identity, SSH state, and account authentication. In a security review, this kind of scope mismatch is dangerous because it can mislead operators into authorizing broader system and account changes than the skill description suggests.

Static analysis

No suspicious patterns detected.