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

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

All URL validation converges in a single Bash function defined at .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:

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:

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:

: "${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:

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 Validates base string safety before specialized checks
assert_safe_sha common.sh Ensures commit SHAs contain only hex characters
assert_safe_path 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:

- 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →