forked from claw/flaskpaste
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:
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user