Files
flaskpaste/documentation/api.md
Username 7deba711d4
All checks were successful
CI / test (push) Successful in 38s
entropy: exempt small content from check
Small data has unreliable entropy measurement due to sample size.
MIN_ENTROPY_SIZE (default 256 bytes) sets the threshold.
2025-12-20 08:48:13 +01:00

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/json for 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: inline is set for image/* and text/* 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:

  1. Magic byte signatures (highest priority)

    • PNG: \x89PNG\r\n\x1a\n
    • JPEG: \xff\xd8\xff
    • GIF: GIF87a or GIF89a
    • WebP: RIFF....WEBP
    • ZIP: PK\x03\x04
    • PDF: %PDF
    • GZIP: \x1f\x8b
  2. Explicit Content-Type header (if not generic)

  3. UTF-8 detection (falls back to text/plain)

  4. Binary fallback (application/octet-stream)


Paste Expiry

Pastes expire based on last access time (default: 5 days).

  • Every GET /{id} or GET /{id}/raw updates the last access timestamp
  • Cleanup runs automatically (hourly, throttled)
  • Configurable via FLASKPASTE_EXPIRY environment 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:

  1. Client requests a challenge via GET /challenge
  2. Server returns a nonce, difficulty, expiry time, and signed token
  3. Client computes SHA256 hashes until finding one with enough leading zero bits
  4. Client submits paste with X-PoW-Token and X-PoW-Solution headers

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.