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.
This commit is contained in:
Username
2025-12-20 08:48:13 +01:00
parent 8addf2d9e8
commit 7deba711d4
4 changed files with 23 additions and 6 deletions

View File

@@ -455,7 +455,8 @@ def create_paste():
# Check minimum entropy requirement (encryption enforcement) # Check minimum entropy requirement (encryption enforcement)
min_entropy = current_app.config.get("MIN_ENTROPY", 0) min_entropy = current_app.config.get("MIN_ENTROPY", 0)
if min_entropy > 0: min_entropy_size = current_app.config.get("MIN_ENTROPY_SIZE", 256)
if min_entropy > 0 and content_size >= min_entropy_size:
entropy = _calculate_entropy(content) entropy = _calculate_entropy(content)
if entropy < min_entropy: if entropy < min_entropy:
current_app.logger.warning( current_app.logger.warning(

View File

@@ -29,8 +29,10 @@ class Config:
# Minimum entropy requirement (0 = disabled) # Minimum entropy requirement (0 = disabled)
# Encrypted data has ~7.5-8.0 bits/byte, plaintext ~4.0-5.0 # Encrypted data has ~7.5-8.0 bits/byte, plaintext ~4.0-5.0
# Set to 7.0+ to effectively require encryption # Set to 6.0+ to effectively require encryption
MIN_ENTROPY = float(os.environ.get("FLASKPASTE_MIN_ENTROPY", 0)) MIN_ENTROPY = float(os.environ.get("FLASKPASTE_MIN_ENTROPY", 0))
# Minimum size for entropy check (small data has unreliable entropy measurement)
MIN_ENTROPY_SIZE = int(os.environ.get("FLASKPASTE_MIN_ENTROPY_SIZE", 256))
# Reverse proxy trust configuration # Reverse proxy trust configuration
# SECURITY: The X-SSL-Client-SHA1 header is trusted for authentication. # SECURITY: The X-SSL-Client-SHA1 header is trusted for authentication.

View File

@@ -355,7 +355,8 @@ FlaskPaste can require minimum content entropy to enforce client-side encryption
export FLASKPASTE_MIN_ENTROPY=6.0 # Require encryption-level entropy (0=disabled) 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) export FLASKPASTE_MIN_ENTROPY_SIZE=256 # Only check content >= this size (default: 256)
``` ```
**Response (400 Bad Request):**
**Response (400 Bad Request):**
```json ```json
{ {
"error": "Content entropy too low", "error": "Content entropy too low",
@@ -369,7 +370,7 @@ export FLASKPASTE_MIN_ENTROPY=7.0 # Require ~encryption-level entropy (0=disabl
- Small data is exempt (configurable via `MIN_ENTROPY_SIZE`, default 256 bytes) - Small data is exempt (configurable via `MIN_ENTROPY_SIZE`, default 256 bytes)
- Compressed data (gzip, zip) also has high entropy — not distinguishable from encrypted - Compressed data (gzip, zip) also has high entropy — not distinguishable from encrypted
- This is a heuristic, not cryptographic proof of encryption - This is a heuristic, not cryptographic proof of encryption
**Recommended thresholds:** **Recommended thresholds:**
| Threshold | Effect | | Threshold | Effect |
|-----------|--------| |-----------|--------|

View File

@@ -250,9 +250,11 @@ class TestEntropyEnforcement:
def test_plaintext_rejected(self, entropy_client): def test_plaintext_rejected(self, entropy_client):
"""Plaintext content should be rejected when entropy required.""" """Plaintext content should be rejected when entropy required."""
# Must be >= MIN_ENTROPY_SIZE (256 bytes) to trigger check
plaintext = b"Hello, this is plain English text. " * 10 # ~350 bytes
response = entropy_client.post( response = entropy_client.post(
"/", "/",
data=b"Hello, this is plain English text with low entropy.", data=plaintext,
content_type="text/plain", content_type="text/plain",
) )
assert response.status_code == 400 assert response.status_code == 400
@@ -287,12 +289,23 @@ class TestEntropyEnforcement:
def test_repeated_bytes_rejected(self, entropy_client): def test_repeated_bytes_rejected(self, entropy_client):
"""Repeated bytes have zero entropy and should be rejected.""" """Repeated bytes have zero entropy and should be rejected."""
# Must be >= MIN_ENTROPY_SIZE (256 bytes) to trigger check
response = entropy_client.post( response = entropy_client.post(
"/", "/",
data=b"a" * 1000, data=b"a" * 500,
content_type="text/plain", content_type="text/plain",
) )
assert response.status_code == 400 assert response.status_code == 400
data = response.get_json() data = response.get_json()
assert data["entropy"] == 0.0 assert data["entropy"] == 0.0
def test_small_content_exempt(self, entropy_client):
"""Small content should be exempt from entropy check."""
# Content < MIN_ENTROPY_SIZE (256 bytes) should pass
response = entropy_client.post(
"/",
data=b"Small plaintext content",
content_type="text/plain",
)
assert response.status_code == 201