Back to skill

Security audit

roadshow-capture-skill

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it says, but it automates access to sensitive roadshow content with weak URL controls, local data retention, and reduced browser isolation.

Review before installing. Use this only for roadshows you are authorized to access and copy, avoid untrusted or shortened URLs, prefer setting the email only for the current process, store outputs in a restricted directory, and consider running the capture in an isolated container or VM until URL validation, sandboxing, dependency pinning, and .env handling are improved.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/roadshow-capture.py:42
Finding
Weak URL Validation Allows Corporate Email Disclosure to Attacker-Controlled Sites<![CDATA[ ## Vulnerability Details **File Location**: `scripts/roadshow-capture.py:42-49`, `scripts/netroadshow-capture.py:63-73`, and `scripts/dealroadshow-capture.py:49-60` **Vulnerability Type**: Improper URL and hostname validation **Risk Level**: High ### Vulnerable Code `scripts/roadshow-capture.py:42-49`: ```python url = args.url.lower() if "netroadshow.com" in url: script = get_script_dir() / "netroadshow-capture.py" elif "dealroadshow.com" in url or "dealroadshow.finsight.com" in url: script = get_script_dir() / "dealroadshow-capture.py" else: sys.exit(1) ``` `scripts/netroadshow-capture.py:63-73`: ```python page = ctx.new_page() print(f"1. Navigating to show URL...") page.goto(args.url, wait_until="networkidle", timeout=30000) time.sleep(2) print(f"2. Filling email: {args.email}") email_input = page.locator("#homeEmailInput").first email_input.fill(args.email) time.sleep(0.3) ``` `scripts/dealroadshow-capture.py:49-60`: ```python page = ctx.new_page() print("1. Loading...") page.goto(args.url, wait_until="networkidle", timeout=30000) time.sleep(2) print("2. Email + Launch...") page.locator("input[type='email']").first.fill(email) time.sleep(0.5) page.get_by_text("Launch Deal Roadshow").click() ``` ### Technical Analysis The dispatcher determines the platform using case-insensitive substring checks against the entire URL. A substring match does not establish that the destination hostname belongs to an approved roadshow service. For example, each of the following attacker-controlled URLs would satisfy the current routing logic: ```text https://attacker.example/?target=dealroadshow.com https://netroadshow.com.attacker.example/fake-show https://attacker.example/dealroadshow.com/login ``` After selecting a platform script, the supplied URL is passed directly to `page.goto()`. The scripts then locate an expected email field and populate it with the configured `NRS_EMAIL` value. The DealRoadShow workflow also clicks a launch contr ...[truncated 1673 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse URLs with `urllib.parse.urlsplit()` rather than searching the raw string. 2. Require the `https` scheme and reject embedded credentials, malformed hostnames, and unexpected ports. 3. Compare the normalized hostname against an explicit allowlist: ```python from urllib.parse import urlsplit PLATFORM_HOSTS = { "netroadshow": {"netroadshow.com", "www.netroadshow.com"}, "dealroadshow": { "dealroadshow.com", "www.dealroadshow.com", "dealroadshow.finsight.com", "finsight.com", "www.finsight.com", }, } def validated_hostname(raw_url): parsed = urlsplit(raw_url) hostname = (parsed.hostname or "").rstrip(".").lower() if parsed.scheme != "https": raise ValueError("Only HTTPS URLs are permitted") if parsed.username or parsed.password: raise ValueError("URLs containing credentials are not permitted") if not hostname: raise ValueError("The URL does not contain a valid hostname") return hostname ``` 4. Match only exact approved hosts. If subdomains are required, use boundary-aware checks such as `host == base` or `host.endswith("." + base)`. 5. Repeat destination validation inside both platform-specific scripts so direct invocation remains safe. 6. Validate redirect destinations before entering the email. Permit only the documented redirect chain and approved hosts. 7. Consider blocking requests to unrelated origins through Playwright routing where compatible with the roadshow applications. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/dealroadshow-capture.py:45
Finding
Chromium Sandbox Disabled While Processing Remote Web Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/dealroadshow-capture.py:45` **Vulnerability Type**: Browser isolation disabled **Risk Level**: High ### Vulnerable Code ```python with sync_playwright() as p: b = p.chromium.launch(headless=True, args=["--no-sandbox"]) ctx = b.new_context(viewport={"width": 1920, "height": 1080}) page = ctx.new_page() ``` The documentation also recommends the same configuration in `SKILL.md:176-180`: ```python browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) ``` ### Technical Analysis The `--no-sandbox` option disables Chromium's operating-system-level renderer sandbox. The sandbox is a defense-in-depth boundary intended to restrict a compromised renderer process from accessing the host environment. This Skill processes complex remote content, including JavaScript, media, and persistent network connections. The weak URL validation in the dispatcher additionally makes it possible to direct the browser to an attacker-controlled page. If that page exploits a Chromium vulnerability, disabling the sandbox can turn a renderer compromise into access under the privileges of the Agent process. The code does not add compensating isolation such as a restricted container, dropped Linux capabilities, a read-only filesystem, or a dedicated unprivileged account. ### Attack Path 1. An attacker causes the Skill to navigate to hostile content, either through an accepted roadshow page containing compromised content or through the URL-validation flaw. 2. The hostile page triggers a browser-engine vulnerability. 3. Because Chromium was launched with `--no-sandbox`, a major containment boundary is absent. 4. Successful exploitation can operate with the permissions of the account running the Skill. 5. The compromised process may access readable files, environment variables, network services, and writable directories available to that account. ### Impact Assessment The maximum scope is the operating- ...[truncated 564 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `--no-sandbox` argument: ```python b = p.chromium.launch(headless=True) ``` 2. Run the Skill as a dedicated unprivileged user without administrative permissions. 3. If the deployment platform cannot support Chromium sandboxing, isolate the complete capture process in a hardened container or virtual machine. 4. Apply a read-only root filesystem and mount only a dedicated output directory as writable. 5. Drop unnecessary Linux capabilities and enable `no-new-privileges`. 6. Do not expose unrelated secrets or sensitive host directories to the capture process. 7. Restrict network access to validated roadshow hosts and required supporting domains. 8. Keep Chromium and Playwright patched and use a reviewed, pinned browser build. 9. Correct the weak URL validation before allowing email entry or rendering untrusted pages. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:14
Finding
Automatically Installed Python Dependencies Are Unpinned and Unverified<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:14-17` **Vulnerability Type**: Unpinned third-party dependencies **Risk Level**: Medium ### Vulnerable Code ```yaml install: - kind: pip packages: [playwright, pillow] ``` The same installation model is documented in `references/openclaw-setup.md:45-55`: ```yaml metadata: openclaw: requires: bins: [python3] env: [NRS_EMAIL] install: - kind: pip packages: [playwright, pillow] ``` ### Technical Analysis The dependency declarations specify package names without exact versions or cryptographic hashes. Every installation can therefore resolve to a different release from the configured Python package index. The package names are consistent with the Skill's functionality, and no typosquatted dependency was identified. However, installation is not reproducible and implicitly trusts all future releases selected by the resolver. A compromised upstream release, compromised package index, or malicious package served through an untrusted index configuration could introduce code that executes during installation or import. The separate `playwright install chromium` instruction also downloads a browser artifact without a project-controlled lock or documented integrity verification. ### Attack Path 1. An upstream package account, distribution artifact, package index, or configured index endpoint is compromised. 2. A malicious or altered release becomes the version selected by `pip`. 3. A user installs or reinstalls the Skill. 4. The package is downloaded without a project-pinned version and hash. 5. Malicious installation or runtime code executes with the privileges of the installing or invoking user. ### Impact Assessment Compromised dependency code could obtain the same privileges as the installation or Skill process. Potential impact includes reading environment variables, accessing local files, making arbitrary network requests, changing generated output, an ...[truncated 174 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin each dependency to an exact reviewed version. 2. Maintain a lock file generated through a controlled dependency-review process. 3. Require hashes during installation, for example through a hash-locked requirements file and `pip install --require-hashes`. 4. Pin Playwright and its corresponding browser revision together to avoid compatibility drift. 5. Use only trusted package indexes and prevent fallback to unintended extra indexes. 6. Scan dependencies for known vulnerabilities before publishing each Skill release. 7. Test and review upgrades in a separate update process rather than automatically accepting the latest versions. 8. Document the expected package source, exact versions, and browser artifact installation procedure. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:40
Finding
Documentation Directs Persistent Plaintext Storage of User Email<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:40-50` **Vulnerability Type**: Plaintext storage of personal data **Risk Level**: Medium ### Vulnerable Code ```text ## First-Time Setup (Email Configuration) 1. Tell the agent your roadshow email 2. It writes to `scripts/.env`: ``` NRS_EMAIL=your-email@your-company.com ``` 3. No further prompts needed If `NRS_EMAIL` is unset, the agent will prompt once and save it to `.env`. No need to set environment variables manually. ``` A similar assertion appears in `references/openclaw-setup.md:63-72`: ```python # Resolve email: CLI arg → env var → agent asks and writes to .env email = args.email or os.environ.get("NRS_EMAIL") assert email, "NRS_EMAIL is not set." ``` The accompanying documentation states that the agent will write the email into `.env`. ### Technical Analysis The setup instructions direct the Agent to retain a user's roadshow email in a plaintext file under the Skill directory. They do not require restrictive permissions, encryption, exclusion from source control, or exclusion from packaged artifacts. No `.gitignore` or equivalent package-exclusion file exists in the reviewed project structure. Consequently, an `.env` file created according to the instructions could be included in an archive, copied with the Skill, committed to a repository, exposed through backups, or read by another local account when permissions allow. There is also a discrepancy between the documented behavior and the implementation. The Python scripts read only the command-line option and `os.environ`; they do not parse `scripts/.env`. The documentation therefore encourages persistent storage that does not provide the promised runtime behavior. ### Attack Path 1. A user gives the Agent a corporate email address as instructed. 2. The Agent writes it to `scripts/.env` in plaintext. 3. The Skill directory is archived, synchronized, published, backed up, or accessed by another local user. 4. The `.env` ...[truncated 635 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the instruction to save the email inside the Skill directory. 2. Prefer a process-scoped environment variable or protected platform secret store. 3. Do not pass the email through `--email` when avoidable because command-line arguments can be visible in process listings and shell history. 4. If persistent local storage is required: - Store it in a user-specific configuration directory rather than the source tree. - Create the file with permission mode `0600`. - Add `.env` to source-control and package-exclusion rules. - Avoid including the file in logs, backups, diagnostic bundles, or published archives. 5. Ensure implementation and documentation agree. Either implement secure configuration loading or remove all claims that `.env` is automatically loaded. 6. Avoid printing the complete email address in operational logs; redact it or omit it. 7. Clearly disclose that the address is sent to the selected roadshow provider before capture begins. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (14)

Credential Access

High
Category
Privilege Escalation
Content
## 脚本中的凭证解析优先级

```python
# Resolve email: CLI arg → env var → agent 提问后写入 .env
email = args.email or os.environ.get("NRS_EMAIL")
assert email, "NRS_EMAIL 未设置。请通过 --email 参数或 export NRS_EMAIL=your-email@company.com 设置邮箱。"
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
print()

    # Run
    env = os.environ.copy()
    proc = subprocess.run(cmd, env=env)
    sys.exit(proc.returncode)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill clearly requires shell execution and environment-variable handling, but it does not declare an explicit tool/permission scope. That creates a governance gap: an agent or platform may grant broader execution than users expect, reducing visibility into sensitive operations like launching browsers, writing files, and reading configuration.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill states that it will save the user's roadshow email into scripts/.env, but it does not present this as a prominent security/privacy warning or discuss retention and access implications. Storing identifiers in a local dotenv file can expose user data to other local processes, users, backups, or accidental commits if the file is not protected.

Session Persistence

Medium
Category
Rogue Agent
Content
✅ pp.evaluate('document.querySelector(".btn-agree").click()')
   ❌ page.get_by_text("Agree").click() — doesn't trigger

4. "Resume previous session" / "Start from beginning" prompt
   Always pick "Start from beginning" (hard rule, no user prompt)

5. URL → /presentation/v2/{id}/MediaSlides
Confidence
55% 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.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The document provides explicit instructions to bypass/auto-accept a legal disclaimer and to capture the full slide deck into screenshots/PDF, but it includes no warning, authorization checks, or guidance about licensing, confidentiality, or privacy constraints. In the context of an investor-roadshow capture skill, this materially increases the risk of unauthorized reproduction and distribution of potentially restricted presentation content.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
The script reads an email address from a command-line argument or environment variable and fills it into a remote page before launching the roadshow. While this is part of the automation flow, the file does not clearly warn the user that personal data from arguments or environment variables will be submitted to an external service.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script captures presentation slides and saves them as PNGs and a PDF under a local output directory, which can expose sensitive investor or deal materials to other local users, backups, or downstream processes if the storage location is not secured. In this skill's context, the entire purpose is to extract and persist presentation content, so the risk is real even if intentional and not overtly malicious.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code captures slide images and writes them to the output directory, then combines them into a PDF. Although file output is part of the tool's function, there is no explicit warning or disclosure that presentation content will be stored locally as PNG files and a PDF, which may affect user data handling expectations.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This code embeds all natural-language descriptions, argument help text, and status/error messages in Chinese only. That creates a locale/language policy concern because users are not given any option to select another language or indication that the tool is intended only for a Chinese-speaking audience.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Run
    env = os.environ.copy()
    proc = subprocess.run(cmd, env=env)
    sys.exit(proc.returncode)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The installation steps tell users to set `NRS_EMAIL` directly in their shell environment, which involves handling personal data, but the document does not warn that this value may persist in shell history or be visible to subprocesses. For markdown files, privacy-affecting behavior should be disclosed when user data is involved.

Natural-Language Policy Violations

Low
Confidence
75% confidence
Finding
The module docstring and assertion messages use Chinese without offering any language choice. Under the language-policy check, this can be a locale/language constraint that is not explicitly documented as region-specific or presented as an opt-in.

Missing User Warnings

Low
Confidence
77% confidence
Finding
The script accesses an environment variable containing an email address and then uses it to interact with the remote site. While the argument help mentions the env var fallback, there is no explicit disclosure that personal data from the environment will be consumed and transmitted to the target service.

Static analysis

No suspicious patterns detected.