06/01 App enhanced
This commit is contained in:
+38
-35
@@ -359,47 +359,50 @@ def webhook():
|
||||
# Header format: Teller-Signature: t=<timestamp>,v1=<sig1>,v1=<sig2>
|
||||
# Signed message: <timestamp>.<raw_body>
|
||||
signing_secret = current_app.config.get('TELLER_WEBHOOK_SECRET', '')
|
||||
if signing_secret:
|
||||
sig_header = request.headers.get('Teller-Signature', '')
|
||||
body = request.get_data()
|
||||
if not signing_secret:
|
||||
log.error('[teller] TELLER_WEBHOOK_SECRET not configured — rejecting webhook')
|
||||
return jsonify({'error': 'Webhook verification not configured'}), 403
|
||||
|
||||
if not sig_header:
|
||||
log.warning('[teller] missing Teller-Signature header')
|
||||
return jsonify({'error': 'Missing signature'}), 401
|
||||
sig_header = request.headers.get('Teller-Signature', '')
|
||||
body = request.get_data()
|
||||
|
||||
# Parse header: t=<timestamp>,v1=<sig>,v1=<sig2>...
|
||||
parts = dict(
|
||||
(p.split('=', 1) if '=' in p else (p, ''))
|
||||
for p in sig_header.split(',')
|
||||
)
|
||||
timestamp = parts.get('t', '')
|
||||
# Collect all v1 signatures (may be multiple during key rotation)
|
||||
signatures = [v for k, v in
|
||||
[p.split('=', 1) for p in sig_header.split(',') if p.startswith('v1=')]
|
||||
]
|
||||
if not sig_header:
|
||||
log.warning('[teller] missing Teller-Signature header')
|
||||
return jsonify({'error': 'Missing signature'}), 401
|
||||
|
||||
if not timestamp or not signatures:
|
||||
log.warning('[teller] malformed Teller-Signature header')
|
||||
return jsonify({'error': 'Invalid signature'}), 401
|
||||
# Parse header: t=<timestamp>,v1=<sig>,v1=<sig2>...
|
||||
parts = dict(
|
||||
(p.split('=', 1) if '=' in p else (p, ''))
|
||||
for p in sig_header.split(',')
|
||||
)
|
||||
timestamp = parts.get('t', '')
|
||||
# Collect all v1 signatures (may be multiple during key rotation)
|
||||
signatures = [v for k, v in
|
||||
[p.split('=', 1) for p in sig_header.split(',') if p.startswith('v1=')]
|
||||
]
|
||||
|
||||
# Reject replays older than 5 minutes
|
||||
import time
|
||||
try:
|
||||
if abs(time.time() - int(timestamp)) > 300:
|
||||
log.warning('[teller] webhook replay attack detected')
|
||||
return jsonify({'error': 'Timestamp too old'}), 401
|
||||
except ValueError:
|
||||
pass
|
||||
if not timestamp or not signatures:
|
||||
log.warning('[teller] malformed Teller-Signature header')
|
||||
return jsonify({'error': 'Invalid signature'}), 401
|
||||
|
||||
# signed_message = timestamp + "." + raw_body
|
||||
signed_message = f'{timestamp}.'.encode() + body
|
||||
expected = hmac.new(
|
||||
signing_secret.encode(), signed_message, hashlib.sha256
|
||||
).hexdigest()
|
||||
# Reject replays older than 5 minutes
|
||||
import time
|
||||
try:
|
||||
if abs(time.time() - int(timestamp)) > 300:
|
||||
log.warning('[teller] webhook replay attack detected')
|
||||
return jsonify({'error': 'Timestamp too old'}), 401
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not any(hmac.compare_digest(expected, sig) for sig in signatures):
|
||||
log.warning('[teller] webhook signature mismatch')
|
||||
return jsonify({'error': 'Invalid signature'}), 401
|
||||
# signed_message = timestamp + "." + raw_body
|
||||
signed_message = f'{timestamp}.'.encode() + body
|
||||
expected = hmac.new(
|
||||
signing_secret.encode(), signed_message, hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
if not any(hmac.compare_digest(expected, sig) for sig in signatures):
|
||||
log.warning('[teller] webhook signature mismatch')
|
||||
return jsonify({'error': 'Invalid signature'}), 401
|
||||
|
||||
# Teller sends GET to verify the endpoint is reachable
|
||||
if request.method == 'GET':
|
||||
|
||||
@@ -261,9 +261,15 @@ def ocr_receipt():
|
||||
if size > 10 * 1024 * 1024:
|
||||
return jsonify({'error': 'File too large (max 10MB)'}), 400
|
||||
|
||||
mime_map = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp'}
|
||||
mime_type = mime_map.get(ext, 'image/jpeg')
|
||||
# Validate actual file content via magic bytes
|
||||
from app.routes.settings import _check_magic
|
||||
header = f.read(12)
|
||||
f.seek(0)
|
||||
magic_ext, magic_mime = _check_magic(header)
|
||||
if magic_ext is None or magic_ext not in ('jpg', 'jpeg', 'png', 'gif', 'webp'):
|
||||
return jsonify({'error': 'File content does not match an allowed image type'}), 400
|
||||
|
||||
mime_type = magic_mime
|
||||
image_bytes = f.read()
|
||||
|
||||
result = extract_from_bytes(image_bytes, mime_type)
|
||||
|
||||
Reference in New Issue
Block a user