Back to skill

Security audit

LinkedIn Post

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a LinkedIn posting helper, but its helper script can be aimed at arbitrary browser tabs or URLs despite documenting a LinkedIn-only workflow.

Review before installing. Use this only with a trusted OpenClaw CLI and browser profile, and do not allow untrusted callers or prompts to supply --open-url, --target-id, --profile, --config, or --publish. Publishing should remain a manual, explicit user-approved action, and the helper should ideally validate the current URL is https://www.linkedin.com/feed/ before filling or clicking.

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
linkedin_post.py:73
Finding
Unrestricted Browser Destination and Target Selection## Vulnerability Details **File Location**: `linkedin_post.py`, lines 73–115 **Vulnerability Type**: Insufficient validation of browser navigation and target scope **Risk Level**: Medium ### Vulnerable Code ```python parser.add_argument("--open-url", default=DEFAULT_FEED_URL, help="URL to open for the share modal") parser.add_argument("--target-id", help="Existing tab target id. Skip opening a new share modal when provided") if args.target_id: target_id = args.target_id else: opened = run_browser(["open", args.open_url], token, args.profile) target_id = parse_target_id(opened) time.sleep(args.wait_seconds) snapshot = run_browser(["snapshot", "--target-id", target_id, "--limit", "300", "--format", "ai"], token, args.profile) textbox_ref = parse_ref( snapshot, [ r'textbox "Text editor for creating content" \[ref=(e\d+)\]', r'textbox "[^\"]*creating content[^\"]*" \[ref=(e\d+)\]', r'textbox "[^\"]*What do you want to talk about\?[^\"]*" \[ref=(e\d+)\]', ], "textbox", ) if args.publish: post_snapshot = run_browser(["snapshot", "--target-id", target_id, "--limit", "220", "--format", "ai"], token, args.profile) post_ref = parse_ref( post_snapshot, [ r'button "Post" \[ref=(e\d+)\]', r'button "게시" \[ref=(e\d+)\]', ], "Post button", ) run_browser(["click", post_ref, "--target-id", target_id], token, args.profile) ``` ### Technical Analysis The skill is intended exclusively for publishing LinkedIn feed posts, but the `--open-url` option accepts an arbitrary URL and `--target-id` accepts an arbitrary existing browser tab. The script does not verify that the selected target is an HTTPS LinkedIn page or that its path corresponds to the intended feed composer. After selecting the target, the script trusts accessible snapshot labels to identify the textbox and final su ...[truncated 1806 chars]
Remediation
## Remediation Suggestions 1. Remove `--open-url` if custom destinations are not required and always use the fixed LinkedIn feed URL. 2. If custom URLs must remain supported, parse them with `urllib.parse.urlparse` and require: - the `https` scheme; - an exact approved hostname such as `www.linkedin.com`; - the intended `/feed/` path; - rejection of embedded credentials, deceptive subdomains, and nonstandard destinations. 3. Before every fill or click operation, query the current target URL and confirm that it remains on an approved LinkedIn origin and path. 4. When `--target-id` is supplied, resolve the target metadata and reject tabs whose current URL is outside the allowlist. 5. Repeat origin validation immediately before clicking the final `Post` button to mitigate redirects or target navigation between snapshots. 6. Preserve the existing explicit `--publish` control and require an additional confirmation mechanism when the caller is interactive. 7. Prefer structured browser selectors scoped to the verified LinkedIn composer rather than relying only on generic accessibility labels.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (3)

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run_browser(args: List[str], token: str, profile: str) -> str:
    env = os.environ.copy()
    env["OPENCLAW_GATEWAY_TOKEN"] = token
    cmd = ["openclaw", "browser", "--browser-profile", profile, *args]
    res = subprocess.run(cmd, capture_output=True, text=True, env=env)
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
90% confidence
Finding
The skill explicitly instructs use of shell commands, environment-based secrets, and local file/config access, but it does not declare any scoped tool permissions or allowed-tools boundary. That creates an overbroad execution surface where an agent may use more capability than intended, including reading sensitive config files or invoking arbitrary shell behavior beyond the narrow LinkedIn posting workflow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
env = os.environ.copy()
    env["OPENCLAW_GATEWAY_TOKEN"] = token
    cmd = ["openclaw", "browser", "--browser-profile", profile, *args]
    res = subprocess.run(cmd, capture_output=True, text=True, env=env)
    if res.returncode != 0:
        raise SystemExit((res.stderr or res.stdout or "browser command failed").strip())
    return res.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Static analysis

No suspicious patterns detected.