security: implement HASH-001 and ENUM-001 remediations

HASH-001: Add threading lock to content hash deduplication
- Prevents race condition between SELECT and UPDATE
- Ensures accurate dedup counting under concurrent load

ENUM-001: Add rate limiting to paste lookups
- Separate rate limiter for GET/HEAD on paste endpoints
- Default 60 requests/minute per IP (configurable)
- Prevents brute-force paste ID enumeration attacks
This commit is contained in:
Username
2025-12-24 23:12:28 +01:00
parent da1beca893
commit c130020ab8
5 changed files with 116 additions and 36 deletions

View File

@@ -5,12 +5,16 @@ from __future__ import annotations
import hashlib
import secrets
import sqlite3
import threading
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any
from flask import current_app, g
# HASH-001: Lock for content hash deduplication to prevent race conditions
_content_hash_lock = threading.Lock()
if TYPE_CHECKING:
from flask import Flask
@@ -264,44 +268,47 @@ def check_content_hash(content_hash: str) -> tuple[bool, int]:
db = get_db()
# Check existing hash record
row = db.execute(
"SELECT count, last_seen FROM content_hashes WHERE hash = ?", (content_hash,)
).fetchone()
# HASH-001: Lock to prevent race condition between SELECT and UPDATE
with _content_hash_lock:
# Check existing hash record
row = db.execute(
"SELECT count, last_seen FROM content_hashes WHERE hash = ?", (content_hash,)
).fetchone()
if row is None:
# First time seeing this content
if row is None:
# First time seeing this content
db.execute(
"INSERT INTO content_hashes (hash, first_seen, last_seen, count) "
"VALUES (?, ?, ?, 1)",
(content_hash, now, now),
)
db.commit()
return True, 1
if row["last_seen"] < cutoff:
# Outside window, reset counter
db.execute(
"UPDATE content_hashes SET first_seen = ?, last_seen = ?, count = 1 WHERE hash = ?",
(now, now, content_hash),
)
db.commit()
return True, 1
# Within window, check threshold
current_count = row["count"] + 1
if current_count > max_count:
# Exceeded threshold, don't increment (prevent counter overflow)
return False, row["count"]
# Update counter
db.execute(
"INSERT INTO content_hashes (hash, first_seen, last_seen, count) VALUES (?, ?, ?, 1)",
(content_hash, now, now),
"UPDATE content_hashes SET last_seen = ?, count = ? WHERE hash = ?",
(now, current_count, content_hash),
)
db.commit()
return True, 1
if row["last_seen"] < cutoff:
# Outside window, reset counter
db.execute(
"UPDATE content_hashes SET first_seen = ?, last_seen = ?, count = 1 WHERE hash = ?",
(now, now, content_hash),
)
db.commit()
return True, 1
# Within window, check threshold
current_count = row["count"] + 1
if current_count > max_count:
# Exceeded threshold, don't increment (prevent counter overflow)
return False, row["count"]
# Update counter
db.execute(
"UPDATE content_hashes SET last_seen = ?, count = ? WHERE hash = ?",
(now, current_count, content_hash),
)
db.commit()
return True, current_count
return True, current_count
def init_app(app: Flask) -> None: