Back to skill

Security audit

Safe-Web

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it says, but needs review because it can fetch any address your machine can reach and its setup can alter system Python and create a system-wide command.

Install only if you are comfortable with a command that can fetch any URL reachable from your machine. Prefer running it in a restricted network environment, avoid the sudo symlink unless needed, use a virtual environment with pinned dependencies instead of --break-system-packages, and treat Brave search as sending your query and API key to Brave.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/safe-web.py:71
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/safe-web.py`, lines 71 and 182–184 **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) ``` ```python # Validate URL parsed = urlparse(args.url) if not parsed.scheme or not parsed.netloc: print(f"Error: Invalid URL: {args.url}", file=sys.stderr) sys.exit(1) ``` ### Technical Analysis The URL validation only verifies that the supplied value has a scheme and network location. It does not: - Restrict the scheme explicitly to HTTP or HTTPS. - Reject loopback, private, link-local, reserved, multicast, or unspecified IP addresses. - Resolve hostnames and validate every resulting IP address. - Protect against DNS rebinding. - Validate redirect destinations. - Limit the size of downloaded responses. The URL is subsequently passed to `requests.get()` with `allow_redirects=True`. Consequently, a caller can instruct the Skill to connect to an internal service directly or use a public endpoint that redirects to an internal address. PromptGuard scans the returned text for prompt-injection patterns, but it is not an SSRF defense. Internal data that does not trigger the prompt-injection threshold can still be printed, returned as JSON, or written to a caller-selected output file. ### Attack Path 1. An attacker causes the Skill to invoke the fetch command with a URL targeting an internal resource, such as: - A loopback service. - A private-network administration interface. - A cloud instance metadata endpoint. - A public URL that redirects to one of these targets. 2. The code confirms only that the URL contains a scheme and network location. 3. `requests.get()` connects to the target and follows redirects automatically. 4. The response body is parsed by BeautifulSoup and scanned by PromptGuard. 5. If the response does not meet the prompt-injection ...[truncated 748 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only explicit `http` and `https` schemes. 2. Reject URLs containing embedded credentials or ambiguous host representations. 3. Resolve the hostname before connecting and reject every address in loopback, private, link-local, reserved, multicast, or unspecified ranges for both IPv4 and IPv6. 4. Disable automatic redirects or validate each redirect destination using the same scheme and IP-address policy. 5. Protect against DNS rebinding by ensuring the validated destination is the address used for the connection. 6. Consider an allowlist of domains when the Skill is used in a controlled environment. 7. Apply maximum response-size and redirect-count limits. 8. Apply outbound firewall or proxy rules so the process cannot reach cloud metadata endpoints or internal management networks. 9. Add automated tests covering direct private addresses, IPv6 loopback, alternate IP representations, DNS names resolving to private addresses, and public-to-private redirects. ]]>

T08 · Insecure Dependencies

Warning
Location
README.md:23
Finding
Unpinned Dependencies Are Installed into the System-Managed Python Environment<![CDATA[ ## Vulnerability Details **File Location**: `README.md`, lines 23–28; duplicated in `SKILL.md`, lines 18–23 **Vulnerability Type**: Unsafe and unpinned dependency installation **Risk Level**: Medium ### Vulnerable Code ```bash # Install PromptGuard first cd /home/linuxbrew/.openclaw/workspace/skills/prompt-guard pip3 install --break-system-packages -e . # Install web dependencies pip3 install --break-system-packages requests beautifulsoup4 ``` The same installation approach appears in `SKILL.md`: ```bash # Install PromptGuard first cd /home/linuxbrew/.openclaw/workspace/skills/prompt-guard pip3 install --break-system-packages -e . # Install web dependencies (if not present) pip3 install --break-system-packages requests beautifulsoup4 ``` ### Technical Analysis The instructions install `requests` and `beautifulsoup4` without exact version or integrity-hash constraints. The artifacts installed during setup can therefore change independently of the audited Skill. The `--break-system-packages` option bypasses Python's externally managed environment protection and permits pip to alter a system-managed Python installation. This can replace or conflict with packages used by other applications. PromptGuard is installed in editable mode from a separately maintained workspace directory. Its version and content are not cryptographically pinned by this project. Python package installation may execute packaging or build logic, so a compromised or unexpectedly modified PromptGuard checkout can run code during installation. No evidence shows that the named dependencies are intentionally malicious. The issue is the unsafe supply-chain and installation model, not a confirmed malicious package. ### Attack Path 1. An attacker compromises an upstream dependency release, package index path, or the separate local PromptGuard checkout. 2. A user follows the documented installation commands. 3. Pip resolves unpinned dependency versions or processes the attacker-m ...[truncated 964 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create and use a dedicated virtual environment instead of passing `--break-system-packages`. 2. Pin every dependency to an audited exact version. 3. Use a lock file with cryptographic hashes, such as a hash-locked requirements file installed with `--require-hashes`. 4. Pin PromptGuard to a reviewed release or immutable commit rather than relying on mutable editable workspace content. 5. Verify the provenance and integrity of the PromptGuard source before installation. 6. Avoid editable installations in production deployments. 7. Publish supported dependency ranges in project metadata while deploying from a fully resolved lock file. 8. Run installation and execution as a non-privileged user. 9. Add dependency vulnerability and provenance scanning to the release process. ]]>
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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (10)

Tainted flow: 'headers' from os.environ.get (line 109, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"Error fetching URL: {e}", file=sys.stderr)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'headers' from os.environ.get (line 109, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(
            'https://api.search.brave.com/res/v1/web/search',
            headers=headers,
            params=params,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Instruction Override

High
Category
Prompt Injection
Content
By default, OpenClaw's native `web_fetch` and `web_search` tools fetch content directly without security scanning. Safe-web provides the same functionality but adds a critical security layer that scans all content for:

- Instruction override attempts ("ignore previous instructions")
- Role manipulation attacks ("you are now DAN")
- System impersonation patterns
- Hidden malicious payloads in web pages
Confidence
80% confidence
Finding
This pattern attempts to override system instructions or ignore safety constraints. Without LLM analysis, manual review is recommended.

YARA rule 'agent_skill_prompt_injection_hidden_instructions': Prompt injection or hidden instructions embedded in AI agent skill text [agent_skills]

High
Category
YARA Match
Content
ning to detect and block prompt injection attacks hidden in web content, emails, PDFs, and documents before they reach the AI.

## Why Use This?

By default, OpenClaw's native `web_fetch` and `web_search` tools fetch content directly without security scanning. Safe-web provides the same functionality but adds a critical security layer that scans all content for:

- Instruction override attempts ("ignore previous instructions")
- Role manipulation attacks ("you are now DAN")
- System impersonation patterns
- Hidden malicious payloads in web pages

## Installation

### 1. Install Dependencies

```bash
# Install PromptGuard first
cd /home/linuxbrew/.openclaw/workspace/skills/prompt-guard
pip3 install --break-system-packages -e .

# Install web dependencies
pip3 install --break-system-packages requests beautifulsoup4
```

### 2. Create Symlink (Optional but Recommended)

```bash
sudo ln -s /home/linuxbrew/.openclaw/workspace/skills/safe-web/scripts/safe-web.py /usr/local/bin/safe-web
```
Confidence
80% confidence
Finding
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### 2. Create Symlink (Optional but Recommended)

```bash
sudo ln -s /home/linuxbrew/.openclaw/workspace/skills/safe-web/scripts/safe-web.py /usr/local/bin/safe-web
```

### 3. Configure Brave API Key (for search)
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### 2. Create Symlink (Optional but Recommended)

```bash
sudo ln -s /home/linuxbrew/.openclaw/workspace/skills/safe-web/scripts/safe-web.py /usr/local/bin/safe-web
```

### 3. Configure Brave API Key (for search)
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

External Transmission

Medium
Category
Data Exfiltration
Content
try:
        response = requests.get(
            'https://api.search.brave.com/res/v1/web/search',
            headers=headers,
            params=params,
            timeout=30
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
for result in results:
        # Scan title
        title_result = scan_with_promptguard(result['title'])
        if title_result.severity.value >= getattr(title_result.severity, severity_threshold).value:
            threats_found.append({
                'field': 'title',
                'url': result['url'],
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
# Scan description
        desc_result = scan_with_promptguard(result['description'])
        if desc_result.severity.value >= getattr(desc_result.severity, severity_threshold).value:
            threats_found.append({
                'field': 'description',
                'url': result['url'],
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
result = scan_with_promptguard(content)
    
    severity_threshold = "MEDIUM" if args.strict else "HIGH"
    should_block = result.severity.value >= getattr(result.severity, severity_threshold).value
    
    if args.json:
        output = {
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

Detected: suspicious.prompt_injection_instructions

Prompt-injection style instruction pattern detected.

Warn
Code
suspicious.prompt_injection_instructions
Location
README.md:13