# SSRF Protection in Claude Plugin Validation: How the GitHub Action Blocks Malicious URLs

> Learn how the Claude plugins community GitHub Action protects against SSRF vulnerabilities with robust URL validation, ensuring secure plugin integration.

- Repository: [Anthropic/claude-plugins-community](https://github.com/anthropics/claude-plugins-community)
- Tags: how-to-guide
- Published: 2026-09-05

---

**The `validate-plugins` GitHub Action in `anthropics/claude-plugins-community` prevents Server-Side Request Forgery (SSRF) by enforcing strict URL validation through the `assert_safe_url` function before any external repository is cloned.**

SSRF attacks allow malicious actors to trick a server into making unauthorized requests to internal services or restricted networks. In CI/CD pipelines that clone external code, this vulnerability is particularly dangerous—attackers could probe cloud metadata endpoints like `http://169.254.169.254` or access internal infrastructure. This article examines how the Claude plugin validation system implements defense-in-depth against SSRF through whitelist-based host validation, scheme enforcement, and pattern matching.

## The Central Guard: `assert_safe_url` in [`common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/common.sh)

All URL validation converges in a single Bash function defined at [`.github/actions/validate-plugins/lib/common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/validate-plugins/lib/common.sh). The `assert_safe_url` function implements a three-layer defense that runs **before** any network request occurs:

### Layer 1: Pattern-Based Scheme Enforcement

The function first validates URL structure with a strict regex:

```bash
if [[ ! "$url" =~ ^https://[A-Za-z0-9./_-]+$ ]]; then
  die "url does not match ^https://[A-Za-z0-9./_-]+$ : $url"
fi

```

This pattern enforces three critical constraints:

- **HTTPS only** — Rejects `http://`, `ftp://`, `file://`, or any other scheme
- **Alphanumeric plus safe characters** — Allows only `/`, `.`, `_`, and `-` in paths
- **No query strings or fragments** — Prevents parameter injection and URL-encoded attacks

### Layer 2: Host Extraction and IP Blocking

After pattern validation, the function extracts and inspects the host component:

```bash
local host="${url#https://}"
host="${host%%/*}"
if [[ "$host" =~ ^[0-9.]+$ ]] || [[ "$host" =~ : ]]; then
  die "url host is a bare IP address (not permitted): $host"
fi

```

This blocks **two major SSRF vectors**:

- **IPv4 addresses** — Patterns like `192.168.1.1` or `169.254.169.254` (AWS metadata) are rejected
- **IPv6 addresses** — Any host containing a colon (`:`) is blocked, eliminating bracket-wrapped IPv6 variants

### Layer 3: Allowlist Enforcement

The final and strictest check validates against an explicit host list:

```bash
: "${ALLOWED_HOSTS:?ALLOWED_HOSTS must be set}"
local allowed="$ALLOWED_HOSTS"
local ok=""
for h in $allowed; do
  if [[ "$host" == "$h" ]] || [[ "$host" == *".$h" ]]; then
    ok=1; break
  fi
done
if [[ -z "$ok" ]]; then
  die "url host '$host' is not in the allowlist ($allowed)"
fi

```

The default `ALLOWED_HOSTS` value permits only:
- `github.com` (and subdomains like `raw.githubusercontent.com`)
- `gitlab.com`
- `bitbucket.org`

Subdomain matching via `*.$h` allows organizational instances (e.g., `corp.github.com`) while maintaining strict root-domain control.

## Invocation Point: Pre-Clone Validation

The SSRF guard executes at the critical moment in [`.github/actions/validate-plugins/scripts/30-validate-cli-external.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/validate-plugins/scripts/30-validate-cli-external.sh):

```bash
url="$(jq -r '.source.url // .source.repo // empty' <<<"$ext")"

# … normalization logic converts owner/repo → https://github.com/… …

assert_safe_url "$url"   # <-- SSRF protection enforced here

git clone --quiet --depth 1 -- "$url" "$dest"

```

This sequence guarantees that **validation occurs after URL construction but before any network operation**. The `--` terminator after `git clone` options prevents the validated URL from being misinterpreted as a command flag.

## Defense-in-Depth: Additional Safety Measures

The validation system extends protection beyond URLs to eliminate secondary injection vectors:

| Function | Location | Purpose |
|----------|----------|---------|
| `assert_safe_string` | [`common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/common.sh) | Validates base string safety before specialized checks |
| `assert_safe_sha` | [`common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/common.sh) | Ensures commit SHAs contain only hex characters |
| `assert_safe_path` | [`common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/common.sh) | Restricts sub-path specifications to safe patterns |

All validated values are re-checked immediately prior to shell interpolation, and the Action consistently uses `--` option terminators with `git` commands.

## Configuring the Allowlist

Operators can customize permitted hosts via the Action's `allowed-hosts` input:

```yaml
- uses: anthropics/claude-plugins-community/.github/actions/validate-plugins@v1
  with:
    allowed-hosts: "github.com gitlab.com gitea.example.com"

```

The input accepts space-separated hostnames. Subdomain matching applies automatically—`gitea.example.com` permits `team.gitea.example.com` but blocks `evil-gitea.example.com`.

## Failure Mode and Security Outcome

When `assert_safe_url` detects a violation, it invokes the `die` helper function, which:

1. Prints the specific check that failed to `stderr`
2. Exits with non-zero status
3. Halts the entire validation Action

This **fail-closed design** ensures that ambiguous URLs never proceed to `git clone`. Attackers cannot bypass protection through encoding tricks, case variations, or URL parsing differences between Bash and Git—the validation logic is entirely self-contained in shell primitives with predictable behavior.

## Summary

- **`assert_safe_url`** in [`common.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/common.sh) provides centralized SSRF protection through three sequential validation layers
- **Scheme restriction** to `https://` eliminates protocol-based attacks
- **IP address blocking** prevents access to cloud metadata services and internal networks
- **Host allowlisting** restricts cloning to explicitly approved domains with automatic subdomain support
- **Pre-clone invocation** in [`30-validate-cli-external.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/30-validate-cli-external.sh) ensures no network request precedes validation
- **Fail-closed behavior** aborts the Action on any validation failure, preventing exploitation

## Frequently Asked Questions

### What happens if a plugin specifies an HTTP URL instead of HTTPS?

The `assert_safe_url` function rejects any URL not matching `^https://[A-Za-z0-9./_-]+$`. An HTTP URL fails this pattern check immediately, triggering `die` and aborting the validation Action with a clear error message.

### Can the allowlist include private GitLab or Bitbucket instances?

Yes. Configure the `allowed-hosts` Action input with your internal hostname: `gitlab.corp.example.com`. The subdomain matching logic (`*.$h`) permits `team.gitlab.corp.example.com` automatically. However, raw IP addresses remain blocked regardless of allowlist configuration.

### How does this protect against DNS rebinding attacks?

DNS rebinding exploits rely on changing DNS records between validation and request. The validation system mitigates this by performing only **syntactic** checks—no DNS resolution occurs in `assert_safe_url`. The actual `git clone` occurs immediately after validation, minimizing the time window for DNS record manipulation. Additionally, IP literal blocking prevents attackers from specifying resolved addresses directly.

### Is IPv6 support possible with this implementation?

Not without code modification. The `[[ "$host" =~ : ]]` check rejects any host containing a colon, which includes all IPv6 addresses (both bare and bracket-wrapped forms). This is an intentional hardening measure—IPv6 addresses complicate private range detection and are rarely needed for public Git hosting.