npm package: @llnvd/openclaw-url-guard
Description: OpenClaw plugin for URL allowlisting/blocklisting in web_fetch and web_search tools
Latest version: 0.3.1

# openclaw-url-guard

OpenClaw plugin that guards web tool access with hostname allowlist/blocklist policy.

> ⚠️ **Disclaimer:** This project is a proof of concept (PoC). It is provided "as is" without
> warranty of any kind. We make no guarantees about its security, reliability, or fitness for
> any particular purpose. Use at your own risk.

## How It Works

This plugin uses OpenClaw's `before_tool_call` hook to intercept calls to `web_fetch` and
`web_search`. When either tool is called:

1. The plugin extracts the URL from the request
2. Checks it against your configured allowlist/blocklist policy
3. Optionally queries URLhaus threat feed
4. Optionally applies trust scoring
5. **Blocks** the call if the URL fails any check, or **allows** it to proceed

This approach is transparent to the LLM — it uses the standard `web_fetch` and `web_search`
tools, but they're guarded by your policy.

## Install

```bash
npm install @llnvd/openclaw-url-guard
```

Or from git:

```bash
git clone https://codeberg.org/llnvd/openclaw-url-guard.git
cd openclaw-url-guard
npm install
npm run build
openclaw plugins install .
```

## Quick Start

Add to your OpenClaw config (`~/.openclaw/openclaw.json`):

```json
{
  "plugins": {
    "entries": {
      "openclaw-url-guard": {
        "enabled": true,
        "config": {
          "mode": "allowlist",
          "allowlist": [
            "docs.python.org",
            "developer.mozilla.org",
            "en.wikipedia.org",
            "github.com",
            "stackoverflow.com",
            "*.stackexchange.com"
          ],
          "blockPrivateIps": true
        }
      }
    }
  }
}
```

Restart the gateway and the plugin will automatically guard all `web_fetch` and `web_search` calls.

## Configuration

### Modes

| Mode | Description |
|------|-------------|
| `allowlist` | Only URLs matching the allowlist are allowed (default) |
| `blocklist` | All URLs allowed except those matching the blocklist |
| `hybrid` | URL must be in allowlist AND not in blocklist |

### Basic Options

```json
{
  "mode": "allowlist",
  "allowlist": ["github.com", "*.githubusercontent.com"],
  "blocklist": ["evil.com"],
  "blockPrivateIps": true
}
```

- **allowlist/blocklist**: Hostname patterns. Use `*.domain.com` for wildcard subdomains.
- **blockPrivateIps**: Block requests to private/internal IPs (default: `true`)

### Threat Feeds

Enable URLhaus threat feed lookups:

```json
{
  "mode": "allowlist",
  "allowlist": ["*"],
  "threatFeeds": {
    "urlhaus": true,
    "mode": "fail-open"
  }
}
```

- `urlhaus`: Enable URLhaus API lookups
- `mode`: `fail-open` (allow on API failure) or `fail-closed` (block on API failure)

### SSRF Protections

SSRF protections are enabled by default. Configure if needed:

```json
{
  "ssrf": {
    "validateDns": true,
    "dnsTimeoutMs": 5000,
    "validateRedirects": true,
    "maxRedirects": 10,
    "allowProtocolDowngrade": false
  }
}
```

- `validateDns`: Resolve hostnames and block if any IP is private/reserved
- `validateRedirects`: Validate each redirect hop against policy + DNS
- `allowProtocolDowngrade`: Allow HTTPS → HTTP redirects (not recommended)

### Per-Workspace Policies

Different workspaces can have different URL policies. Workspace policies are **restrictive-only** — they can tighten but never loosen the global policy:

```json
{
  "mode": "allowlist",
  "allowlist": ["github.com", "docs.python.org"],
  "blocklist": ["malware.com"],
  "workspaces": {
    "/home/user/fintech-project": {
      "allowlist": ["api.stripe.com"]
    },
    "/home/user/research": {
      "blocklist": ["competitor.com"]
    }
  }
}
```

- **Allowlist intersection** — Workspace restricts global (only domains in BOTH allowed)
- **Blocklist union** — Workspace extends global (domains in EITHER blocked)
- **Exact path matching** — Policies apply to exact paths, not subdirectories
- **Mode inherited** — Workspaces cannot change the global mode

See [Configuration](./docs/configuration.md#per-workspace-policies) for details.

### Trust Scoring

Optional scoring system for fine-grained control:

```json
{
  "mode": "allowlist",
  "allowlist": ["*"],
  "scoring": {
    "enabled": true,
    "defaultScore": 0,
    "minScore": -6,
    "rules": [
      { "domain": "github.com", "score": 10, "reason": "highly trusted" },
      { "domain": "*.sketch.com", "score": -5, "reason": "known suspicious" }
    ]
  }
}
```

Score ranges:
- `+10` to `+7`: Trusted (skip threat feed checks)
- `+6` to `+3`: Preferred
- `+2` to `-2`: Neutral
- `-3` to `-6`: Suspicious
- `-7` to `-10`: Blocked

> **Note:** Policy (allowlist/blocklist) is **always enforced first**. A blocked URL cannot bypass policy with a high score.

## Security Defaults

- Only `http://` and `https://` protocols accepted
- Private/internal IP targets blocked by default
- SSRF protections enabled by default:
  - DNS resolution validation (blocks hostnames resolving to private IPs)
  - Redirect chain validation (validates each hop, blocks protocol downgrades)
  - Hostname blocking (`localhost`, `*.local`)
- URLhaus lookups timeout after 5 seconds
- Invalid URLs are blocked
- **Policy is always enforced before scoring** — blocked URLs cannot bypass policy with high scores

## Logging

Enable verbose logging to see guard decisions:

```json
{
  "logging": {
    "enabled": true,
    "logBlocked": true,
    "verboseErrors": true
  }
}
```

## Development

```bash
npm install
npm run build
npm test
```

Run E2E tests:

```bash
npm run test:e2e
```

## Documentation

- [Configuration](./docs/configuration.md)
- [API Reference](./docs/api-reference.md)
- [Trust Scoring](./docs/scoring.md)
- [Testing](./docs/testing.md)

## License

MIT