Small data has unreliable entropy measurement due to sample size. MIN_ENTROPY_SIZE (default 256 bytes) sets the threshold.
11 KiB
FlaskPaste API Reference
Overview
FlaskPaste provides a RESTful API for creating, retrieving, and deleting text and binary pastes.
Base URL: http://your-server:5000/
Content Types:
- Requests:
application/json,text/plain,application/octet-stream, or any binary type - Responses:
application/jsonfor metadata, original MIME type for raw content
Authentication
Authentication is optional and uses client certificate fingerprints passed via the X-SSL-Client-SHA1 header.
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
The fingerprint must be exactly 40 lowercase hexadecimal characters (SHA1).
Benefits of authentication:
- Larger upload limit (50 MiB vs 3 MiB)
- Ability to delete owned pastes
Endpoints
GET /challenge
Get a proof-of-work challenge for paste creation. Required when PoW is enabled.
Request:
GET /challenge HTTP/1.1
Host: localhost:5000
Response (PoW disabled):
{
"enabled": false,
"difficulty": 0
}
Response (PoW enabled):
{
"enabled": true,
"nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8",
"difficulty": 20,
"expires": 1700000300,
"token": "a1b2c3d4...:1700000300:20:signature"
}
The client must find a number N such that SHA256(nonce + ":" + N) has at least difficulty leading zero bits.
GET /health
Health check endpoint for load balancers and monitoring.
Request:
GET /health HTTP/1.1
Host: localhost:5000
Response (200 OK):
{
"status": "healthy",
"database": "ok"
}
Response (503 Service Unavailable):
{
"status": "unhealthy",
"database": "error"
}
GET /
Returns API information and usage instructions.
Request:
GET / HTTP/1.1
Host: localhost:5000
Response:
{
"name": "FlaskPaste",
"version": "1.1.0",
"endpoints": {
"GET /": "API information",
"GET /health": "Health check",
"POST /": "Create paste",
"GET /<id>": "Retrieve paste metadata",
"GET /<id>/raw": "Retrieve raw paste content",
"DELETE /<id>": "Delete paste"
},
"usage": {
"raw": "curl --data-binary @file.txt http://host/",
"pipe": "cat file.txt | curl --data-binary @- http://host/",
"json": "curl -H 'Content-Type: application/json' -d '{\"content\":\"...\"}' http://host/"
},
"note": "Use --data-binary (not -d) to preserve newlines"
}
POST /
Create a new paste.
Request (Raw Binary):
POST / HTTP/1.1
Host: localhost:5000
Content-Type: application/octet-stream
<binary content>
Request (JSON):
POST / HTTP/1.1
Host: localhost:5000
Content-Type: application/json
{"content": "Hello, World!"}
Request (with Proof-of-Work):
When PoW is enabled, include the challenge token and solution:
POST / HTTP/1.1
Host: localhost:5000
Content-Type: text/plain
X-PoW-Token: a1b2c3d4...:1700000300:20:signature
X-PoW-Solution: 12345678
Hello, World!
Response (201 Created):
{
"id": "abc12345",
"url": "/abc12345",
"raw": "/abc12345/raw",
"mime_type": "text/plain",
"created_at": 1700000000,
"owner": "a1b2c3..." // Only present if authenticated
}
Errors:
| Code | Description |
|---|---|
| 400 | No content provided |
| 400 | Proof-of-work required (when PoW enabled) |
| 400 | Proof-of-work failed (invalid/expired challenge) |
| 413 | Paste too large |
| 429 | Duplicate content rate limit exceeded |
Size Limits:
- Anonymous: 3 MiB (configurable via
FLASKPASTE_MAX_ANON) - Authenticated: 50 MiB (configurable via
FLASKPASTE_MAX_AUTH)
GET /{id}
HEAD /{id}
Retrieve paste metadata. HEAD returns headers only (no body).
Request:
GET /abc12345 HTTP/1.1
Host: localhost:5000
Response (200 OK):
{
"id": "abc12345",
"mime_type": "text/plain",
"size": 1234,
"created_at": 1700000000,
"raw": "/abc12345/raw"
}
Errors:
| Code | Description |
|---|---|
| 400 | Invalid paste ID format |
| 404 | Paste not found |
GET /{id}/raw
HEAD /{id}/raw
Retrieve raw paste content with correct MIME type. HEAD returns headers only (useful for checking MIME type and size without downloading content).
Request:
GET /abc12345/raw HTTP/1.1
Host: localhost:5000
Response (200 OK):
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: inline
<binary content>
Content-Disposition: inlineis set forimage/*andtext/*types- Content-Type matches the detected/stored MIME type
Errors:
| Code | Description |
|---|---|
| 400 | Invalid paste ID format |
| 404 | Paste not found |
DELETE /{id}
Delete a paste. Requires authentication and ownership.
Request:
DELETE /abc12345 HTTP/1.1
Host: localhost:5000
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Response (200 OK):
{
"message": "Paste deleted"
}
Errors:
| Code | Description |
|---|---|
| 400 | Invalid paste ID format |
| 401 | Authentication required |
| 403 | Permission denied (not owner) |
| 404 | Paste not found |
MIME Type Detection
FlaskPaste automatically detects MIME types using:
-
Magic byte signatures (highest priority)
- PNG:
\x89PNG\r\n\x1a\n - JPEG:
\xff\xd8\xff - GIF:
GIF87aorGIF89a - WebP:
RIFF....WEBP - ZIP:
PK\x03\x04 - PDF:
%PDF - GZIP:
\x1f\x8b
- PNG:
-
Explicit Content-Type header (if not generic)
-
UTF-8 detection (falls back to
text/plain) -
Binary fallback (
application/octet-stream)
Paste Expiry
Pastes expire based on last access time (default: 5 days).
- Every
GET /{id}orGET /{id}/rawupdates the last access timestamp - Cleanup runs automatically (hourly, throttled)
- Configurable via
FLASKPASTE_EXPIRYenvironment variable
Abuse Prevention
FlaskPaste includes content-hash based deduplication to prevent spam and abuse.
How it works:
- Each paste's SHA256 content hash is tracked
- Repeated submissions of identical content are throttled
- After exceeding the threshold, further duplicates are rejected with 429
Default limits:
- Window: 1 hour (
FLASKPASTE_DEDUP_WINDOW) - Maximum: 3 identical submissions per window (
FLASKPASTE_DEDUP_MAX)
Response (429 Too Many Requests):
{
"error": "Duplicate content rate limit exceeded",
"count": 3,
"window_seconds": 3600
}
Configuration:
export FLASKPASTE_DEDUP_WINDOW=3600 # Window in seconds (default: 1 hour)
export FLASKPASTE_DEDUP_MAX=3 # Max duplicates per window (default: 3)
Notes:
- Different content is not affected by other content's limits
- Counter resets after the window expires
- Hash records are cleaned up periodically
Entropy Enforcement
FlaskPaste can require minimum content entropy to enforce client-side encryption.
How it works:
- Shannon entropy is calculated for submitted content (bits per byte)
- Encrypted/random data: ~7.5-8.0 bits/byte
- English text: ~4.0-5.0 bits/byte
- Content below threshold is rejected with 400
Configuration:
export FLASKPASTE_MIN_ENTROPY=6.0 # Require encryption-level entropy (0=disabled)
export FLASKPASTE_MIN_ENTROPY_SIZE=256 # Only check content >= this size (default: 256)
Response (400 Bad Request):
{
"error": "Content entropy too low",
"entropy": 4.12,
"min_entropy": 7.0,
"hint": "Encrypt content before uploading (-e flag in fpaste)"
}
Caveats:
- Small data is exempt (configurable via
MIN_ENTROPY_SIZE, default 256 bytes) - Compressed data (gzip, zip) also has high entropy — not distinguishable from encrypted
- This is a heuristic, not cryptographic proof of encryption
Recommended thresholds:
| Threshold | Effect |
|---|---|
| 0 | Disabled (default) |
| 5.0 | Blocks most plaintext |
| 6.0 | Requires encryption or compression |
| 7.0 | Requires encryption + sufficient size |
Proof-of-Work
FlaskPaste includes an optional proof-of-work system to prevent automated spam.
How it works:
- Client requests a challenge via
GET /challenge - Server returns a nonce, difficulty, expiry time, and signed token
- Client computes SHA256 hashes until finding one with enough leading zero bits
- Client submits paste with
X-PoW-TokenandX-PoW-Solutionheaders
Algorithm:
For N = 0, 1, 2, ...:
hash = SHA256(nonce + ":" + N)
if leading_zero_bits(hash) >= difficulty:
return N as solution
Configuration:
export FLASKPASTE_POW_DIFFICULTY=20 # Leading zero bits required (0=disabled)
export FLASKPASTE_POW_TTL=300 # Challenge validity in seconds
export FLASKPASTE_POW_SECRET="..." # Optional signing key (auto-generated if empty)
Headers:
| Header | Description |
|---|---|
X-PoW-Token |
The full token from /challenge response |
X-PoW-Solution |
The computed solution number |
Error responses:
{
"error": "Proof-of-work required",
"hint": "GET /challenge for a new challenge"
}
{
"error": "Proof-of-work failed: Challenge expired"
}
Notes:
- Difficulty 20 requires approximately 1 million hash attempts on average
- Challenges are signed to prevent tampering
- Each challenge can only be used once (nonce uniqueness)
Error Response Format
All errors return JSON:
{
"error": "Description of the error"
}
For size limit errors (413):
{
"error": "Paste too large",
"size": 5000000,
"max_size": 3145728,
"authenticated": false
}
Security Headers
All responses include the following security headers:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
X-XSS-Protection |
1; mode=block |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
default-src 'none'; frame-ancestors 'none' |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Cache-Control |
no-store, no-cache, must-revalidate, private |
Pragma |
no-cache |
Request Tracing
All requests include an X-Request-ID header for log correlation:
- If the client provides
X-Request-ID, it is passed through - If not provided, a UUID is generated
- The ID is echoed back in the response
- All log entries include
[rid=<request-id>]
Example:
# Client-provided ID
curl -H "X-Request-ID: my-trace-123" https://paste.example.com/health
# Response includes:
# X-Request-ID: my-trace-123
Proxy Trust Validation
When FLASKPASTE_PROXY_SECRET is configured, the application validates that requests come from a trusted reverse proxy by checking the X-Proxy-Secret header.
This provides defense-in-depth against header spoofing if an attacker bypasses the reverse proxy.
Configuration:
export FLASKPASTE_PROXY_SECRET="your-secret-value"
Proxy Configuration (HAProxy):
http-request set-header X-Proxy-Secret your-secret-value
If the secret doesn't match, authentication headers (X-SSL-Client-SHA1) are ignored and the request is treated as anonymous.