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