forked from ComputerTech/bastebin
Security: headers, input validation, db connection safety
This commit is contained in:
parent
89a7c33adb
commit
8339d0f2d9
62
app.py
62
app.py
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue