Security: headers, input validation, db connection safety

This commit is contained in:
ComputerTech 2026-03-27 01:30:48 +00:00
parent 89a7c33adb
commit 8339d0f2d9
1 changed files with 49 additions and 13 deletions

62
app.py
View File

@ -34,7 +34,29 @@ app.config['SECRET_KEY'] = _server.get('secret_key', 'change-me')
DATABASE = _db_cfg.get('path', 'bastebin.db')
MAX_ENCRYPTED_BYTES = _pastes.get('max_size_bytes', 2 * 1024 * 1024)
MAX_TITLE_BYTES = 200
_ENCRYPTED_RE = re.compile(r'^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$')
_LANGUAGE_RE = re.compile(r'^[a-z0-9_+-]{1,32}$')
_PASTE_ID_RE = re.compile(r'^[0-9a-f]{4,32}$')
# ── Security headers ─────────────────────────────────────────────────────────
@app.after_request
def set_security_headers(response):
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['Referrer-Policy'] = 'same-origin'
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline'; "
"style-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline'; "
"img-src 'self' data:; "
"connect-src 'self' blob:; "
"object-src 'none'; "
"base-uri 'self'; "
"frame-ancestors 'none';"
)
return response
# ── Jinja helpers ─────────────────────────────────────────────────────────────
@ -120,6 +142,12 @@ def create_paste():
return jsonify({'error': 'content must be a non-empty string'}), 400
if len(content.encode('utf-8')) > MAX_ENCRYPTED_BYTES:
return jsonify({'error': 'Paste too large'}), 413
# Enforce server-side limits on title and language
if not isinstance(title, str):
title = 'Untitled'
title = title.strip()[:MAX_TITLE_BYTES] or 'Untitled'
if not isinstance(language, str) or not _LANGUAGE_RE.match(language):
language = _pastes.get('default_language', 'text')
store_data = json.dumps({'title': title, 'content': content, 'language': language})
else:
return jsonify({'error': 'Provide either encrypted_data or content'}), 400
@ -141,26 +169,34 @@ def create_paste():
paste_id = generate_paste_id()
conn = get_db_connection()
conn.execute(
'INSERT INTO pastes (id, encrypted_data, expires_at) VALUES (?, ?, ?)',
(paste_id, store_data, expires_at)
)
conn.commit()
conn.close()
try:
conn.execute(
'INSERT INTO pastes (id, encrypted_data, expires_at) VALUES (?, ?, ?)',
(paste_id, store_data, expires_at)
)
conn.commit()
finally:
conn.close()
return jsonify({'paste_id': paste_id, 'url': f'/{paste_id}'})
@app.route('/<paste_id>')
@app.route('/<string:paste_id>')
def view_paste(paste_id):
if not _PASTE_ID_RE.match(paste_id):
abort(404)
paste = _get_paste_or_abort(paste_id)
conn = get_db_connection()
conn.execute('UPDATE pastes SET views = views + 1 WHERE id = ?', (paste_id,))
conn.commit()
paste = conn.execute('SELECT * FROM pastes WHERE id = ?', (paste_id,)).fetchone()
conn.close()
try:
conn.execute('UPDATE pastes SET views = views + 1 WHERE id = ?', (paste_id,))
conn.commit()
paste = conn.execute('SELECT * FROM pastes WHERE id = ?', (paste_id,)).fetchone()
finally:
conn.close()
return render_template('view.html', paste=paste)
@app.route('/<paste_id>/raw')
@app.route('/<string:paste_id>/raw')
def view_paste_raw(paste_id):
if not _PASTE_ID_RE.match(paste_id):
abort(404)
paste = _get_paste_or_abort(paste_id)
stored = paste['encrypted_data']
@ -168,7 +204,7 @@ def view_paste_raw(paste_id):
if not re.match(r'^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$', stored):
try:
data = json.loads(stored)
return Response(data.get('content', ''), mimetype='text/plain; charset=utf-8')
return Response(data.get('content', ''), mimetype='text/plain')
except (json.JSONDecodeError, TypeError):
pass