Fix security headers for API routes to ensure compatibility with CURL interface
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
26a36de4de
commit
35f80738a7
|
@ -7,9 +7,9 @@ services:
|
|||
dockerfile: Dockerfile
|
||||
image: ploughshares:dev
|
||||
ports:
|
||||
- "5001:5001"
|
||||
- "5005:5000"
|
||||
environment:
|
||||
- FLASK_RUN_PORT=5001
|
||||
- FLASK_RUN_PORT=5000
|
||||
- FLASK_ENV=development
|
||||
- FLASK_DEBUG=1
|
||||
- POSTGRES_HOST=db
|
||||
|
@ -18,6 +18,10 @@ services:
|
|||
- POSTGRES_USER=ploughshares
|
||||
- POSTGRES_PASSWORD=ploughshares_password
|
||||
- APP_ENV=development
|
||||
- CSP_JS_HASH=default_js_hash
|
||||
- CSP_CSS_HASH=default_css_hash
|
||||
- CSP_CUSTOM_CSS_HASH=default_custom_css_hash
|
||||
command: python app.py
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
|
@ -25,7 +25,7 @@ COPY static/ ./static/
|
|||
# Create uploads directory
|
||||
RUN mkdir -p uploads
|
||||
|
||||
# Calculate CSP hashes for static resources
|
||||
# Calculate CSP hashes for static resources and create environment variables
|
||||
RUN echo "Calculating CSP hashes for static resources..." && \
|
||||
# Calculate hash for JS files
|
||||
JS_HASH=$(openssl dgst -sha256 -binary static/js/bootstrap.bundle.min.js | openssl base64) && \
|
||||
|
@ -35,17 +35,19 @@ RUN echo "Calculating CSP hashes for static resources..." && \
|
|||
echo "CSS hash: $CSS_HASH" && \
|
||||
CUSTOM_CSS_HASH=$(openssl dgst -sha256 -binary static/css/custom.css | openssl base64) && \
|
||||
echo "Custom CSS hash: $CUSTOM_CSS_HASH" && \
|
||||
# Create environment variables file for CSP hashes
|
||||
echo "export CSP_JS_HASH=$JS_HASH" > /app/csp_hashes.env && \
|
||||
echo "export CSP_CSS_HASH=$CSS_HASH" >> /app/csp_hashes.env && \
|
||||
echo "export CSP_CUSTOM_CSS_HASH=$CUSTOM_CSS_HASH" >> /app/csp_hashes.env
|
||||
# Export CSP hashes as environment variables directly
|
||||
echo "export CSP_JS_HASH=\"$JS_HASH\"" > /app/csp_hashes.env && \
|
||||
echo "export CSP_CSS_HASH=\"$CSS_HASH\"" >> /app/csp_hashes.env && \
|
||||
echo "export CSP_CUSTOM_CSS_HASH=\"$CUSTOM_CSS_HASH\"" >> /app/csp_hashes.env && \
|
||||
# Make the file executable
|
||||
chmod +x /app/csp_hashes.env
|
||||
|
||||
# Set build argument for APP_VERSION
|
||||
ARG APP_VERSION=unknown
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5001
|
||||
EXPOSE 5000
|
||||
|
||||
# Command to run the application with CSP hashes
|
||||
CMD ["/bin/bash", "-c", "source /app/csp_hashes.env && exec python app.py"]
|
|
@ -7,6 +7,7 @@ from datetime import datetime
|
|||
import locale
|
||||
import logging
|
||||
from flask_talisman import Talisman
|
||||
import re
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
|
@ -53,19 +54,19 @@ if CSP_CSS_HASH:
|
|||
if CSP_CUSTOM_CSS_HASH:
|
||||
logger.info(f"Using pre-calculated CSP hash for custom CSS: sha256-{CSP_CUSTOM_CSS_HASH}")
|
||||
|
||||
# Configure security headers with Talisman
|
||||
# Base CSP settings with pre-calculated hashes for static resources
|
||||
csp = {
|
||||
'default-src': ["'self'"],
|
||||
# Define the base CSP for UI routes
|
||||
ui_csp = {
|
||||
'default-src': "'none'", # Deny by default
|
||||
'script-src': ["'self'"] + ([f"'sha256-{CSP_JS_HASH}'"] if CSP_JS_HASH else []),
|
||||
'style-src': ["'self'"] +
|
||||
([f"'sha256-{CSP_CSS_HASH}'"] if CSP_CSS_HASH else []) +
|
||||
([f"'sha256-{CSP_CUSTOM_CSS_HASH}'"] if CSP_CUSTOM_CSS_HASH else []),
|
||||
([f"'sha256-{CSP_CSS_HASH}'"] if CSP_CSS_HASH else []) +
|
||||
([f"'sha256-{CSP_CUSTOM_CSS_HASH}'"] if CSP_CUSTOM_CSS_HASH else []),
|
||||
'img-src': ["'self'", "data:"],
|
||||
'font-src': ["'self'"],
|
||||
'connect-src': "'self'",
|
||||
'object-src': "'none'",
|
||||
'frame-ancestors': "'none'",
|
||||
'manifest-src': "'self'",
|
||||
'object-src': "'none'", # Explicitly disallow objects
|
||||
'frame-ancestors': "'none'", # Prevent framing
|
||||
'base-uri': "'self'",
|
||||
'form-action': "'self'"
|
||||
}
|
||||
|
@ -74,18 +75,18 @@ csp = {
|
|||
if APP_DOMAIN:
|
||||
logger.info(f"Configuring CSP for domain: {APP_DOMAIN}")
|
||||
# Add domain to connect-src if needed
|
||||
if APP_DOMAIN not in csp['connect-src']:
|
||||
if isinstance(csp['connect-src'], list):
|
||||
csp['connect-src'].append(APP_DOMAIN)
|
||||
if APP_DOMAIN not in ui_csp['connect-src']:
|
||||
if isinstance(ui_csp['connect-src'], list):
|
||||
ui_csp['connect-src'].append(APP_DOMAIN)
|
||||
else:
|
||||
csp['connect-src'] = [csp['connect-src'], APP_DOMAIN]
|
||||
ui_csp['connect-src'] = [ui_csp['connect-src'], APP_DOMAIN]
|
||||
|
||||
# Update form-action to include the domain
|
||||
if isinstance(csp['form-action'], list):
|
||||
if APP_DOMAIN not in csp['form-action']:
|
||||
csp['form-action'].append(APP_DOMAIN)
|
||||
if isinstance(ui_csp['form-action'], list):
|
||||
if APP_DOMAIN not in ui_csp['form-action']:
|
||||
ui_csp['form-action'].append(APP_DOMAIN)
|
||||
else:
|
||||
csp['form-action'] = [csp['form-action'], APP_DOMAIN]
|
||||
ui_csp['form-action'] = [ui_csp['form-action'], APP_DOMAIN]
|
||||
|
||||
# Configure Permissions-Policy (formerly Feature-Policy)
|
||||
# Deny access to all browser features that we don't need
|
||||
|
@ -131,10 +132,17 @@ additional_headers = {
|
|||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
|
||||
# Initialize Talisman
|
||||
# Custom function to determine if CSP should be applied
|
||||
def csp_for_request():
|
||||
# Disable CSP for API routes
|
||||
if request.path.startswith('/api/'):
|
||||
return None
|
||||
return ui_csp
|
||||
|
||||
# Initialize Talisman with a dynamic CSP function
|
||||
talisman = Talisman(
|
||||
app,
|
||||
content_security_policy=csp,
|
||||
content_security_policy=csp_for_request,
|
||||
content_security_policy_nonce_in=['script-src'],
|
||||
feature_policy=permissions_policy,
|
||||
force_https=force_https,
|
||||
|
@ -149,13 +157,44 @@ talisman = Talisman(
|
|||
session_cookie_http_only=True
|
||||
)
|
||||
|
||||
# Add additional security headers that Talisman doesn't support natively
|
||||
# Add CORS headers for API routes
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
for header, value in additional_headers.items():
|
||||
response.headers[header] = value
|
||||
def add_api_headers(response):
|
||||
if request.path.startswith('/api/'):
|
||||
# Add CORS headers for API routes
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
response.headers['Cross-Origin-Resource-Policy'] = 'cross-origin'
|
||||
|
||||
# Ensure CSP is completely removed for API routes
|
||||
if 'Content-Security-Policy' in response.headers:
|
||||
del response.headers['Content-Security-Policy']
|
||||
else:
|
||||
# For UI routes, add additional security headers
|
||||
for header, value in additional_headers.items():
|
||||
response.headers[header] = value
|
||||
|
||||
return response
|
||||
|
||||
# API route handler for OPTIONS requests (CORS preflight)
|
||||
@app.route('/api/<path:path>', methods=['OPTIONS'])
|
||||
def api_options(path):
|
||||
response = jsonify({'status': 'ok'})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
# Create a test API endpoint to verify the CSP settings
|
||||
@app.route('/api/test', methods=['GET'])
|
||||
def api_test():
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'message': 'API endpoint is working correctly',
|
||||
'version': VERSION
|
||||
})
|
||||
|
||||
# Set locale for currency formatting
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||
|
|
Loading…
Reference in New Issue