add proof-of-work spam prevention
All checks were successful
CI / test (push) Successful in 37s

Clients must solve a SHA256 hash puzzle before paste creation.
Configurable via FLASKPASTE_POW_DIFFICULTY (0 = disabled, 16 = default).
Challenge tokens expire after FLASKPASTE_POW_TTL seconds (default 300).
This commit is contained in:
Username
2025-12-20 04:03:59 +01:00
parent 682df17257
commit 8fdeeaed9c
4 changed files with 392 additions and 0 deletions

59
fpaste
View File

@@ -2,6 +2,7 @@
"""FlaskPaste command-line client."""
import argparse
import hashlib
import json
import os
import sys
@@ -54,6 +55,53 @@ def die(msg, code=1):
sys.exit(code)
def solve_pow(nonce, difficulty):
"""Solve proof-of-work challenge.
Find a number N such that SHA256(nonce:N) has `difficulty` leading zero bits.
"""
n = 0
target_bytes = (difficulty + 7) // 8 # Bytes to check
while True:
work = f"{nonce}:{n}".encode()
hash_bytes = hashlib.sha256(work).digest()
# Count leading zero bits
zero_bits = 0
for byte in hash_bytes[:target_bytes + 1]:
if byte == 0:
zero_bits += 8
else:
zero_bits += (8 - byte.bit_length())
break
if zero_bits >= difficulty:
return n
n += 1
# Progress indicator for high difficulty
if n % 100000 == 0:
print(f"\rsolving pow: {n} attempts...", end="", file=sys.stderr)
return n
def get_challenge(config):
"""Fetch PoW challenge from server."""
url = config["server"].rstrip("/") + "/challenge"
status, body, _ = request(url)
if status != 200:
return None
data = json.loads(body)
if not data.get("enabled"):
return None
return data
def cmd_create(args, config):
"""Create a new paste."""
# Read content from file or stdin
@@ -78,6 +126,17 @@ def cmd_create(args, config):
if config["cert_sha1"]:
headers["X-SSL-Client-SHA1"] = config["cert_sha1"]
# Get and solve PoW challenge if required
challenge = get_challenge(config)
if challenge:
if not args.quiet:
print(f"solving pow (difficulty={challenge['difficulty']})...", end="", file=sys.stderr)
solution = solve_pow(challenge["nonce"], challenge["difficulty"])
if not args.quiet:
print(f" done", file=sys.stderr)
headers["X-PoW-Token"] = challenge["token"]
headers["X-PoW-Solution"] = str(solution)
url = config["server"].rstrip("/") + "/"
status, body, _ = request(url, method="POST", data=content, headers=headers)