forked from claw/flaskpaste
add content-hash dedup for abuse prevention
Throttle repeated submissions of identical content using SHA256 hash tracking. Configurable via FLASKPASTE_DEDUP_WINDOW and FLASKPASTE_DEDUP_MAX.
This commit is contained in:
@@ -19,6 +19,16 @@ CREATE TABLE IF NOT EXISTS pastes (
|
||||
CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_pastes_owner ON pastes(owner);
|
||||
CREATE INDEX IF NOT EXISTS idx_pastes_last_accessed ON pastes(last_accessed);
|
||||
|
||||
-- Content hash tracking for abuse prevention
|
||||
CREATE TABLE IF NOT EXISTS content_hashes (
|
||||
hash TEXT PRIMARY KEY,
|
||||
first_seen INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_content_hashes_last_seen ON content_hashes(last_seen);
|
||||
"""
|
||||
|
||||
# Hold reference for in-memory shared cache databases
|
||||
@@ -88,6 +98,79 @@ def cleanup_expired_pastes() -> int:
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def cleanup_expired_hashes() -> int:
|
||||
"""Delete content hashes outside the dedup window.
|
||||
|
||||
Returns number of deleted hashes.
|
||||
"""
|
||||
window = current_app.config["CONTENT_DEDUP_WINDOW"]
|
||||
cutoff = int(time.time()) - window
|
||||
|
||||
db = get_db()
|
||||
cursor = db.execute("DELETE FROM content_hashes WHERE last_seen < ?", (cutoff,))
|
||||
db.commit()
|
||||
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def check_content_hash(content_hash: str) -> tuple[bool, int]:
|
||||
"""Check if content hash exceeds dedup threshold.
|
||||
|
||||
Args:
|
||||
content_hash: SHA256 hex digest of content
|
||||
|
||||
Returns:
|
||||
Tuple of (is_allowed, current_count)
|
||||
is_allowed is False if threshold exceeded within window
|
||||
"""
|
||||
window = current_app.config["CONTENT_DEDUP_WINDOW"]
|
||||
max_count = current_app.config["CONTENT_DEDUP_MAX"]
|
||||
now = int(time.time())
|
||||
cutoff = now - window
|
||||
|
||||
db = get_db()
|
||||
|
||||
# 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
|
||||
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(
|
||||
"UPDATE content_hashes SET last_seen = ?, count = ? WHERE hash = ?",
|
||||
(now, current_count, content_hash)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return True, current_count
|
||||
|
||||
|
||||
def init_app(app) -> None:
|
||||
"""Register database functions with Flask app."""
|
||||
app.teardown_appcontext(close_db)
|
||||
|
||||
Reference in New Issue
Block a user