forked from username/flaskpaste
docs: add MIME detection security assessment
This commit is contained in:
241
documentation/mime-security-assessment.md
Normal file
241
documentation/mime-security-assessment.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# MIME Detection Security Assessment
|
||||
|
||||
Penetration testing of FlaskPaste's magic byte-based MIME detection system.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Assessment Date:** 2025-12-25
|
||||
**Target:** FlaskPaste MIME detection (`app/api/routes.py`)
|
||||
**Methodology:** Polyglot file attacks, magic byte spoofing, header injection
|
||||
**Result:** No critical vulnerabilities found. Defense-in-depth approach effective.
|
||||
|
||||
---
|
||||
|
||||
## Attack Surface
|
||||
|
||||
### MIME Detection Implementation
|
||||
|
||||
```python
|
||||
# Location: app/api/routes.py
|
||||
MAX_MAGIC_LEN = 16 # Maximum bytes checked for magic signatures
|
||||
MAGIC_SIGNATURES = {
|
||||
b"\x89PNG\r\n\x1a\n": "image/png",
|
||||
b"\xff\xd8\xff": "image/jpeg",
|
||||
b"GIF87a": "image/gif",
|
||||
b"GIF89a": "image/gif",
|
||||
# ... 42 total signatures
|
||||
}
|
||||
```
|
||||
|
||||
Detection checks first 16 bytes against known magic byte patterns.
|
||||
|
||||
---
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
### Tools Used
|
||||
- `fpaste` CLI client (with PoW solving)
|
||||
- `curl` for raw HTTP requests
|
||||
- `xxd` for binary file creation
|
||||
- Python scripts for complex payloads
|
||||
|
||||
### Test Categories
|
||||
1. Standard MIME detection validation
|
||||
2. Polyglot file attacks
|
||||
3. Magic byte boundary conditions
|
||||
4. Header injection attempts
|
||||
5. Security header verification
|
||||
|
||||
---
|
||||
|
||||
## Test Results
|
||||
|
||||
### Phase 1: Standard MIME Detection
|
||||
|
||||
| Format | Magic Bytes | Detected MIME | Status |
|
||||
|--------|-------------|---------------|--------|
|
||||
| PNG | `89 50 4E 47` | image/png | PASS |
|
||||
| JPEG | `FF D8 FF E0` | image/jpeg | PASS |
|
||||
| GIF | `47 49 46 38 39 61` | image/gif | PASS |
|
||||
| PDF | `25 50 44 46` | application/pdf | PASS |
|
||||
| ZIP | `50 4B 03 04` | application/zip | PASS |
|
||||
| GZIP | `1F 8B 08` | application/gzip | PASS |
|
||||
| SQLite | `53 51 4C 69 74 65` | application/x-sqlite3 | PASS |
|
||||
| ELF | `7F 45 4C 46` | application/x-executable | PASS |
|
||||
| PE/EXE | `4D 5A` | application/x-msdownload | PASS |
|
||||
|
||||
### Phase 2: Polyglot File Attacks
|
||||
|
||||
#### Test 1: PNG + HTML Payload
|
||||
```bash
|
||||
# Payload: PNG magic bytes + HTML after detection window
|
||||
echo -e '\x89PNG\r\n\x1a\n\x00\x00\x00\x00<script>alert(1)</script>' > poly.bin
|
||||
```
|
||||
**Result:**
|
||||
- Detected MIME: `image/png` (first magic wins)
|
||||
- Served Content-Type: `image/png`
|
||||
- **SAFE:** HTML payload ignored, served as image
|
||||
|
||||
#### Test 2: SVG with Embedded JavaScript
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<script>alert('XSS')</script>
|
||||
</svg>
|
||||
```
|
||||
**Result:**
|
||||
- Detected MIME: `text/plain` (no XML magic in signatures)
|
||||
- Served Content-Type: `text/plain; charset=utf-8`
|
||||
- **SAFE:** Not served as SVG, scripts won't execute
|
||||
|
||||
#### Test 3: GIF89a JavaScript Polyglot
|
||||
```
|
||||
GIF89a/**/=1;alert(1)//
|
||||
```
|
||||
**Result:**
|
||||
- Detected MIME: `image/gif`
|
||||
- Served Content-Type: `image/gif`
|
||||
- **SAFE:** Even if loaded as `<script>`, CSP blocks execution
|
||||
|
||||
#### Test 4: JPEG + PHP Polyglot
|
||||
```bash
|
||||
echo -e '\xff\xd8\xff\xe0\x00\x10JFIF\x00<?php system($_GET["c"]); ?>' > poly.jpg
|
||||
```
|
||||
**Result:**
|
||||
- Detected MIME: `image/jpeg`
|
||||
- **SAFE:** No PHP execution on server, served as raw bytes
|
||||
|
||||
#### Test 5: PDF + ZIP Polyglot
|
||||
```
|
||||
%PDF-1.4
|
||||
...
|
||||
%%EOF
|
||||
PK<ZIP_CONTENT>
|
||||
```
|
||||
**Result:**
|
||||
- Detected MIME: `application/pdf` (PDF checked before ZIP)
|
||||
- **SAFE:** First magic wins, predictable behavior
|
||||
|
||||
### Phase 3: Boundary Condition Tests
|
||||
|
||||
| Test | Payload | Result |
|
||||
|------|---------|--------|
|
||||
| 2-byte PNG prefix | `\x89P` | `application/octet-stream` (incomplete magic) |
|
||||
| PNG + ELF embedded | PNG header + ELF binary | `image/png` (first magic wins) |
|
||||
| Null byte prefix | `\x00\x00<html>...` | `text/plain` |
|
||||
| Header injection | `GIF89a\r\nContent-Type: text/html` | `image/gif` (headers not parsed from content) |
|
||||
|
||||
### Phase 4: Security Headers
|
||||
|
||||
```
|
||||
Content-Type: <detected-mime>
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
|
||||
```
|
||||
|
||||
**Defense Analysis:**
|
||||
|
||||
| Header | Purpose | Effectiveness |
|
||||
|--------|---------|---------------|
|
||||
| `X-Content-Type-Options: nosniff` | Prevents MIME sniffing | Blocks browser interpretation override |
|
||||
| `Content-Security-Policy: default-src 'none'` | Blocks all content execution | Scripts/styles/frames blocked |
|
||||
| `X-Frame-Options: DENY` | Prevents framing | Clickjacking protection |
|
||||
|
||||
---
|
||||
|
||||
## Vulnerability Assessment
|
||||
|
||||
### Attempted Attacks
|
||||
|
||||
| Attack Vector | Technique | Result |
|
||||
|---------------|-----------|--------|
|
||||
| XSS via polyglot | GIF/PNG + JavaScript | BLOCKED by CSP |
|
||||
| MIME confusion | SVG with script | Served as text/plain |
|
||||
| Server-side execution | PHP in JPEG | Not executed (no PHP handler) |
|
||||
| Header injection | CRLF in content | Not reflected in HTTP headers |
|
||||
| Path traversal | `../` in content | No file path derived from content |
|
||||
|
||||
### Security Controls Summary
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DEFENSE IN DEPTH LAYERS │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: Magic byte detection (first 16 bytes only) │
|
||||
│ Layer 2: X-Content-Type-Options: nosniff │
|
||||
│ Layer 3: Content-Security-Policy: default-src 'none' │
|
||||
│ Layer 4: No server-side content processing/execution │
|
||||
│ Layer 5: Content stored as opaque blobs, never interpreted │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands Executed
|
||||
|
||||
### Test File Creation
|
||||
```bash
|
||||
# PNG with HTML payload
|
||||
echo -e '\x89PNG\r\n\x1a\n<script>alert(1)</script>' > poly.bin
|
||||
|
||||
# SVG with script
|
||||
cat > poly.svg << 'EOF'
|
||||
<?xml version="1.0"?><svg><script>alert(1)</script></svg>
|
||||
EOF
|
||||
|
||||
# GIF+JS polyglot
|
||||
echo 'GIF89a/**/=1;alert(1)//' > poly.gif
|
||||
|
||||
# JPEG+PHP
|
||||
echo -e '\xff\xd8\xff\xe0\x00\x10JFIF\x00<?php system($_GET["c"]); ?>' > poly.jpg
|
||||
```
|
||||
|
||||
### Upload and Verify
|
||||
```bash
|
||||
# Upload with fpaste
|
||||
id=$(./fpaste -s https://mymx.me/paste create -E -q poly.bin)
|
||||
|
||||
# Check detected MIME
|
||||
curl -s "https://mymx.me/paste/$id?info" | jq .mime_type
|
||||
|
||||
# Verify served headers
|
||||
curl -sI "https://mymx.me/paste/$id/raw" | grep -i content-type
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Current Implementation: SECURE
|
||||
|
||||
The current implementation provides adequate protection through:
|
||||
|
||||
1. **Deterministic detection:** First matching magic bytes determine MIME type
|
||||
2. **Safe defaults:** Unknown content served as `application/octet-stream` or `text/plain`
|
||||
3. **Strong CSP:** `default-src 'none'` prevents script execution
|
||||
4. **No sniffing:** `X-Content-Type-Options: nosniff` honored by browsers
|
||||
5. **No processing:** Content stored and served as-is, never executed
|
||||
|
||||
### Potential Enhancements (Low Priority)
|
||||
|
||||
1. **Add SVG detection** with forced `text/plain` serving (prevents accidental SVG execution in older browsers)
|
||||
2. **Content-Disposition: attachment** for all downloads (forces download instead of inline display)
|
||||
3. **Sandbox iframe serving** for HTML-like content (additional isolation layer)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The FlaskPaste MIME detection system is resistant to polyglot file attacks due to:
|
||||
|
||||
- Deterministic magic byte matching (first match wins)
|
||||
- Strong security headers preventing browser-side exploitation
|
||||
- No server-side content interpretation or execution
|
||||
- Safe fallback to `text/plain` or `application/octet-stream`
|
||||
|
||||
**Risk Level: LOW**
|
||||
|
||||
The attack surface is minimal because detected MIME types only affect the `Content-Type` header served to clients, and browser-side exploitation is blocked by CSP and X-Content-Type-Options headers.
|
||||
Reference in New Issue
Block a user