#!/usr/bin/env python3
"""Media upload hardening: reject executable extensions, sanitise SVG, keep real media working."""
import json
import tempfile
import uuid
from support import Client, Site
def multipart(fields, files):
boundary = uuid.uuid4().hex
body = b""
for key, value in fields.items():
body += (f"--{boundary}\r\nContent-Disposition: form-data; name=\"{key}\"\r\n\r\n{value}\r\n").encode()
for key, (name, data, ctype) in files.items():
body += (f"--{boundary}\r\nContent-Disposition: form-data; name=\"{key}\"; filename=\"{name}\"\r\n"
f"Content-Type: {ctype}\r\n\r\n").encode() + data + b"\r\n"
body += f"--{boundary}--\r\n".encode()
return body, f"multipart/form-data; boundary={boundary}"
# Real magic bytes so finfo reports an allowed MIME type; a naive check would pass these.
JPEG = bytes.fromhex("ffd8ffe000104a46494600010100000100010000") + b""
MP3 = b"ID3\x03\x00\x00\x00\x00\x00\x00" + b"\xff\xfb\x90\x00" * 40 + b""
PNG = bytes.fromhex("89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
"0000000a49444154789c6360000000020001") + bytes.fromhex("00000000")
SVG_XSS = b''
def upload(client, action, field, name, data, extra=None):
fields = {"action": action, "csrf_token": client.csrf, "type": "image"}
fields.update(extra or {})
body, ctype = multipart(fields, {field: (name, data, "application/octet-stream")})
status, _, raw = client.request("/admin/api.php", body, "POST", {"Content-Type": ctype})
return status, json.loads(raw)
def test_upload_hardening(site):
editor = Client(site).login("editor")
# 1. Polyglot .php uploads must be refused, even with valid image/audio magic bytes.
for action, field, name, data in (("upload-image", "image", "shell.php", JPEG),
("upload-audio", "audio", "shell.php", MP3),
("upload-media", "image", "shell.php", JPEG)):
_, result = upload(editor, action, field, name, data)
assert not result["success"], f"{action} accepted an executable extension: {result}"
# No .php file may have reached a web-served directory.
for folder in ("assets/images", "assets/audio"):
leftovers = list((site.root / folder).rglob("*.php"))
assert not leftovers, f"Executable file written to {folder}: {leftovers}"
# 2. A legitimate PNG still uploads (we did not over-block).
_, result = upload(editor, "upload-image", "image", "photo.png", PNG)
assert result["success"], f"Legitimate PNG rejected: {result}"
# 3. An SVG uploads but is sanitised: no script/onload survive on disk or over HTTP.
_, result = upload(editor, "upload-media", "image", "logo.svg", SVG_XSS)
assert result["success"], f"SVG upload failed: {result}"
stored = (site.root / "assets/images" / result["data"]["name"]).read_text()
assert "