Back to skill

Security audit

Phy App Launcher

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly does what it advertises, but it ships a hard-coded external API credential and generates persistent launcher scripts with unsafe path and command handling.

Install only if you are comfortable with a skill that writes persistent macOS .app launchers which run project commands in Terminal. Avoid using --auto-icon unless the embedded FAL credential has been removed and you supply your own approved key. Do not run this on untrusted projects or project paths until app names, paths, and generated commands are validated and safely escaped.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/create_launcher.py:22
Finding
Hard-Coded FAL API Credential Exposed in Source Code<![CDATA[ ## Vulnerability Details **File Location**: `scripts/create_launcher.py:22-29` **Vulnerability Type**: Hard-coded secret **Risk Level**: High ### Vulnerable Code ```python # Optional: FAL AI for icon generation try: import fal_client FAL_AVAILABLE = True # Set FAL API key if not already set if not os.environ.get("FAL_KEY"): os.environ["FAL_KEY"] = "[REDACTED: hard-coded FAL API credential]" except ImportError: FAL_AVAILABLE = False ``` The original source contains the complete reusable FAL API credential at the redacted location. ### Technical Analysis The script embeds a plaintext FAL API credential and automatically assigns it to the `FAL_KEY` environment variable whenever the user has not configured another key. Anyone able to download or inspect the Skill can extract and reuse this credential independently of the launcher. Environment variables do not protect a secret that is already present in distributed source code. Source archives, repository clones, backups, logs, and installed copies may retain the credential even if it is subsequently removed from the latest version. The credential is used by `generate_icon_with_fal()` when the `--auto-icon` option is enabled: ```python result = fal_client.subscribe( "fal-ai/flux/schnell", arguments={ "prompt": prompt, "image_size": "square", "num_images": 1, }, ) ``` ### Attack Path 1. An attacker obtains the publicly distributed Skill or otherwise reads `scripts/create_launcher.py`. 2. The attacker extracts the hard-coded `FAL_KEY` value. 3. The attacker configures the credential in their own process or directly uses it with the FAL API. 4. API requests are charged to or counted against the credential owner's account until the key is revoked or restricted. No execution of the Skill is required to exploit the exposure. ### Impact Assessment An attacker may obtain unauthorized access to the FAL API capabilities permitted by the ex ...[truncated 489 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Revoke and rotate the exposed FAL credential immediately. Removing it from the current file is insufficient because historical copies may remain accessible. 2. Remove all default credential assignment from the source: ```python fal_key = os.environ.get("FAL_KEY") if not fal_key: raise RuntimeError( "FAL_KEY is required for --auto-icon. " "Configure it through the environment or a secret manager." ) ``` 3. Obtain credentials from a protected environment variable, macOS Keychain, or an approved secret-management service. 4. Ensure credentials are excluded from repositories, packaged artifacts, examples, tests, and logs. 5. Review API usage and billing records for unauthorized activity. 6. Apply provider-side restrictions where available, including minimum required scopes, usage limits, expiration, and account alerts. 7. Add automated secret scanning to source-control and release pipelines. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/create_launcher.py:224
Finding
Path Traversal Enables Unsafe Recursive Directory Deletion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/create_launcher.py:224-231` **Vulnerability Type**: Path traversal and unsafe recursive deletion **Risk Level**: High ### Vulnerable Code ```python # Create .app bundle structure app_bundle = output_dir / f"{app_name}.app" contents_dir = app_bundle / "Contents" macos_dir = contents_dir / "MacOS" resources_dir = contents_dir / "Resources" # Remove existing bundle if present if app_bundle.exists(): shutil.rmtree(app_bundle) ``` The value of `app_name` is supplied through `--name` or derived from the inspected project's `package.json`: ```python if app_name is None: app_name = project_info["name"] ``` For Node.js projects, that detected name can originate from project-controlled JSON: ```python return { "type": "node", "cmd": "npm run dev", "name": pkg.get("name", project_path.name) } ``` ### Technical Analysis `app_name` is treated as a trusted filename even though it can contain absolute paths, path separators, or parent-directory components such as `../`. Appending `.app` does not prevent traversal. For example, an app name such as `../../target` produces a destination resembling: ```text <output_dir>/../../target.app ``` If an absolute value is supplied, `pathlib` can also discard the preceding `output_dir` when composing the path. The code does not resolve the final destination and verify that it remains beneath the intended output directory. The risk is amplified by: ```python shutil.rmtree(app_bundle) ``` If the attacker-selected destination already exists, the script recursively deletes it before creating the new launcher. There is no check that the destination is an application bundle previously generated by this tool. ### Attack Path 1. An attacker prepares a Node.js project containing a crafted `package.json` name with traversal components, or convinces the user to invoke the script with a malicious `--name`. 2. The user runs the documented automatic lau ...[truncated 1063 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat the display name and filesystem bundle name as separate values. 2. Reject names containing: - `/` or `\` - `.` or `..` path components - Absolute paths - NUL bytes, newlines, or other control characters - Empty or whitespace-only values 3. Restrict bundle filenames to a conservative allowlist, for example letters, digits, spaces, hyphens, and underscores. 4. Resolve and validate the destination before any deletion: ```python output_dir = Path(output_dir).expanduser().resolve() output_dir.mkdir(parents=True, exist_ok=True) if not app_name or Path(app_name).name != app_name: raise ValueError("App name must be a single filename") app_bundle = (output_dir / f"{app_name}.app").resolve() if app_bundle.parent != output_dir: raise ValueError("App bundle must be directly inside the output directory") ``` 5. Before recursive deletion, verify that the target: - Is directly beneath the validated output directory. - Has the expected `.app` suffix. - Is not a symbolic link. - Contains a marker proving that this tool created it. 6. Prefer refusing to overwrite an existing destination unless the user supplies an explicit `--force` option. 7. Consider writing to a temporary sibling directory and atomically renaming it only after successful bundle generation. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/create_launcher.py:242
Finding
Shell and AppleScript Injection Through Unescaped Launcher Values<![CDATA[ ## Vulnerability Details **File Location**: `scripts/create_launcher.py:242-253` **Vulnerability Type**: Command injection **Risk Level**: High ### Vulnerable Code ```python # Create the launcher script launcher_script = macos_dir / "launcher" script_content = f'''#!/bin/bash # Auto-generated launcher for {app_name} cd "{project_path}" # Open new Terminal window and run the dev server osascript -e 'tell application "Terminal" activate do script "cd \\"{project_path}\\" && {venv_activate}{start_cmd}" end tell' ''' launcher_script.write_text(script_content) ``` ### Technical Analysis The code constructs executable Bash and AppleScript source through direct string interpolation. Values such as `project_path` and `app_name` are inserted without escaping for their destination language. This involves multiple nested parsing contexts: 1. The generated launcher is parsed by Bash. 2. The single-quoted argument is passed to `osascript`. 3. AppleScript parses the `do script` string. 4. Terminal executes the resulting command through a shell. Escaping suitable for one layer is not necessarily safe for another. In particular, the generated line: ```bash cd "{project_path}" ``` remains vulnerable to shell command substitution if a project directory contains constructs such as `$(...)` or backticks, because command substitution is evaluated inside double-quoted Bash strings. The nested AppleScript command is also susceptible to quote and line-break injection through specially crafted filesystem paths. Additionally, `app_name` is inserted into a generated comment without newline filtering. A multiline project-controlled package name can terminate the comment and add executable shell lines. Although `--cmd` intentionally accepts a command to execute, direct interpolation becomes especially dangerous when commands or other values are obtained from untrusted automation or passed on behalf of another user. ### Attack Path One concrete exploitatio ...[truncated 1687 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not construct nested Bash and AppleScript programs by interpolating untrusted strings. 2. Validate `app_name` using a strict allowlist and reject all newlines and control characters. 3. Quote every shell argument with `shlex.quote()` when generating shell source: ```python import shlex quoted_project = shlex.quote(str(project_path)) ``` 4. Escape AppleScript string values independently from shell values. Do not reuse shell escaping for AppleScript syntax. 5. Prefer passing the project path as a positional argument to a fixed launcher script rather than embedding it into executable source. 6. Consider generating a launcher that invokes a fixed helper executable using an argument array, thereby avoiding nested command parsing. 7. If Terminal automation remains necessary, place the command in a separately generated script with validated arguments and instruct Terminal to execute only the fixed, safely quoted script path. 8. Clearly document that `--cmd` is executable code and require explicit user confirmation when its value originates from an untrusted source. 9. Add tests using paths and names containing spaces, quotes, backticks, `$()`, semicolons, backslashes, Unicode, and newline characters to verify that they cannot alter the generated program structure. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (27)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared purpose is only local launcher creation, but the detected behavior includes external network/service usage, remote content downloading, and API credential injection via an environment variable. That mismatch is dangerous because users and policy engines may authorize the skill expecting only local file generation while it can also exfiltrate metadata, fetch untrusted content, or invoke third-party services with hidden credentials.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The code embeds a FAL API credential and silently injects it into the environment at runtime. Hardcoded secrets are inherently unsafe because they can be extracted, reused by unauthorized parties, incur billing abuse, and normalize hidden external access in a skill that appears local-only.

Missing User Warnings

High
Confidence
99% confidence
Finding
This code reads a hardcoded credential into process environment state without clear user warning or consent. That creates secret-exposure and unauthorized-service-use risk, especially because the skill can trigger network behavior under a seemingly local feature set.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill advertises commands that invoke a local Python script to create .app bundles and launch local services, but it declares no explicit tool scope or permissions. In a skill system, missing scope around shell, file write, environment access, and possible network use increases the chance the agent will execute higher-risk actions without clear user-visible constraints or policy enforcement.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: app-launcher
description: Create macOS desktop app launchers for dev projects. Click an icon on Desktop → auto-runs local dev server in Terminal. Use when user wants to make an app "persistent", create a desktop shortcut, launch dev server with one click, or says "make this app clickable" / "create launcher" / "desktop icon for this project".
homepage: https://canlah.ai
---
Confidence
90% confidence
Finding
The skill is explicitly designed to create a persistent desktop launcher that repeatedly runs a local dev server outside the immediate agent session. Session persistence is risky because it creates an enduring execution path on the user's desktop that can later run commands with a single click, potentially outliving the user's original context or review.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The trigger phrases are broad enough to match ordinary requests like making an app clickable or persistent, which can cause the skill to activate in situations where the user did not intend launcher creation or shell execution. Over-broad activation is especially risky here because the skill performs file creation and may run project commands, increasing the likelihood of unintended side effects.

Session Persistence

Medium
Category
Rogue Agent
Content
## Workflow

1. User says "create launcher for this project" or "make desktop icon"
2. Identify project path and any custom requirements (name, icon, command)
3. Run the script with appropriate options
4. Confirm the .app was created on Desktop
Confidence
88% confidence
Finding
The documented workflow instructs the agent to create a desktop .app that persists after the session, reinforcing the creation of a long-lived execution artifact. In the context of a skill that can auto-detect commands and possibly perform undeclared network-related actions, that persistence increases risk because unsafe behavior can be packaged into a reusable launcher.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill's stated purpose is local launcher creation, but enabling --auto-icon sends app metadata in prompts to a third-party service and downloads remote content without strong disclosure or isolation. This broadens the trust boundary unexpectedly and can leak project/app names or introduce untrusted remote files into the workflow.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The function performs an external network request and downloads remote image content without meaningful disclosure that user-provided app names are transmitted to a third party. In a local-launcher skill, this is an unexpected data flow and could expose sensitive project identities or fetch untrusted content.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
resized.save(output_file, "PNG")

        # Convert iconset to icns using iconutil
        result = subprocess.run(
            ["iconutil", "-c", "icns", iconset_dir, "-o", icns_path],
            capture_output=True,
            text=True
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
launcher_script.write_text(script_content)
    launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    # Create Info.plist
    info_plist = contents_dir / "Info.plist"
    bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")

    plist_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
Confidence
75% 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.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
sizes = [16, 32, 64, 128, 256, 512]
                        for size in sizes:
                            subprocess.run([
                                "sips", "-z", str(size), str(size),
                                str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}.png")
                            ], capture_output=True)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}.png")
                            ], capture_output=True)
                            if size <= 256:
                                subprocess.run([
                                    "sips", "-z", str(size*2), str(size*2),
                                    str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}@2x.png")
                                ], capture_output=True)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Static analysis

No suspicious patterns detected.