Back to skill

Security audit

Webapp Testing

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed Playwright webapp testing helper, but users should treat its server-launch helper as trusted local command execution.

Install only if you are comfortable with a testing helper that can run local commands you provide, such as npm run dev or backend startup scripts. Review the exact --server commands, use it only in trusted workspaces, and check for leftover server processes after runs.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/with_server.py:69
Finding
Server Child Processes May Survive Lifecycle Cleanup<![CDATA[ ## Vulnerability Details **File Location**: `scripts/with_server.py`, lines 69–100 **Vulnerability Type**: Incomplete process-tree termination **Risk Level**: Medium ### Vulnerable Code ```python process = subprocess.Popen( server['cmd'], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) server_processes.append(process) # Wait for this server to be ready print(f"Waiting for server on port {server['port']}...") if not is_server_ready(server['port'], timeout=args.timeout): raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s") print(f"Server ready on port {server['port']}") print(f"\nAll {len(servers)} server(s) ready") # Run the command print(f"Running: {' '.join(args.command)}\n") result = subprocess.run(args.command) sys.exit(result.returncode) finally: # Clean up all servers print(f"\nStopping {len(server_processes)} server(s)...") for i, process in enumerate(server_processes): try: process.terminate() process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait() print(f"Server {i+1} stopped") ``` ### Technical Analysis Each server command is launched with `shell=True`. Consequently, the `Popen` object can represent an intermediary shell rather than the actual development server or all processes created by the command. The cleanup routine sends termination or kill signals only to the process represented by that object. No separate process session or process group is created, and cleanup does not enumerate or terminate descendants. Commands involving package managers, shell pipelines, background jobs, or scripts that spawn child processes can therefore leave descendants running after the intermediary shell exits. Killing the shell does not reliably propagate termination to its children. The use of `stdout=subprocess.PIPE` and `stderr=subprocess.PIPE` without con ...[truncated 1979 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Create an isolated process group or session for every server and terminate the entire group during cleanup. On POSIX systems: 1. Pass `start_new_session=True` to `subprocess.Popen`. 2. On cleanup, send `SIGTERM` to the process group with `os.killpg(process.pid, signal.SIGTERM)`. 3. Wait for graceful shutdown. 4. If the timeout expires, send `SIGKILL` to the same process group. 5. Handle `ProcessLookupError` to account for processes that have already exited. Example: ```python import os import signal import subprocess process = subprocess.Popen( server['cmd'], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True, ) # During cleanup: try: os.killpg(process.pid, signal.SIGTERM) process.wait(timeout=5) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL) process.wait() except ProcessLookupError: pass ``` Use an equivalent process-tree termination mechanism on Windows, such as a new process group combined with an appropriate job object or `taskkill /T`. Where possible, avoid `shell=True` and accept server commands as argument arrays. If shell syntax is required, document that the supplied command is trusted and still isolate its complete process group. Finally, inherit output streams, redirect them to files, or drain both pipes continuously so verbose servers cannot block when pipe buffers fill. ]]>
Vulnerability Patterns
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (7)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description frames the skill as Playwright-based webapp testing, but the documented behavior includes starting arbitrary shell commands, waiting on ports, and executing follow-up commands. That mismatch is dangerous because users or orchestration systems may trust the skill as a limited browser-testing utility while it actually has broader process-execution capability that could be abused to run unintended local commands.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")

            # Use shell=True to support commands with cd and &&
            process = subprocess.Popen(
                server['cmd'],
                shell=True,
                stdout=subprocess.PIPE,
Confidence
99% confidence
Finding
Because the tool's purpose is to orchestrate local webapp testing, it is likely to be run in developer environments with access to source code, secrets, SSH keys, and browser session data; this context increases the blast radius of shell-based parameter injection. A malicious project, test configuration, or copied command line could abuse --server to run arbitrary payloads while appearing to start a dev server.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")

            # Use shell=True to support commands with cd and &&
            process = subprocess.Popen(
                server['cmd'],
                shell=True,
                stdout=subprocess.PIPE,
Confidence
99% confidence
Finding
Because the tool's purpose is to orchestrate local webapp testing, it is likely to be run in developer environments with access to source code, secrets, SSH keys, and browser session data; this context increases the blast radius of shell-based parameter injection. A malicious project, test configuration, or copied command line could abuse --server to run arbitrary payloads while appearing to start a dev server.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill exposes shell execution and file-writing capabilities without declaring any explicit tool scope or permission boundaries. In an agent environment, this creates ambiguity about what the skill is allowed to do and increases the chance that a caller will invoke powerful operations without adequate review or sandboxing.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")

            # Use shell=True to support commands with cd and &&
            process = subprocess.Popen(
                server['cmd'],
                shell=True,
                stdout=subprocess.PIPE,
Confidence
98% confidence
Finding
The script passes a caller-controlled string directly to subprocess.Popen with shell=True, allowing shell metacharacters, command substitution, and chaining operators to be interpreted. If an attacker can influence the --server value, they can execute arbitrary OS commands under the privileges of the script, which is especially risky in an automation/testing tool that routinely launches local services.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Run the command
        print(f"Running: {' '.join(args.command)}\n")
        result = subprocess.run(args.command)
        sys.exit(result.returncode)

    finally:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The script saves screenshots to /mnt/user-data/outputs at L17 and L29, which is a file-write operation affecting user-accessible storage. While comments describe taking screenshots, there is no user-facing prompt, warning, or disclosure that image files will be created.

Static analysis

No suspicious patterns detected.