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
|
dockerfile: Dockerfile
|
||||||
image: ploughshares:dev
|
image: ploughshares:dev
|
||||||
ports:
|
ports:
|
||||||
- "5001:5001"
|
- "5005:5000"
|
||||||
environment:
|
environment:
|
||||||
- FLASK_RUN_PORT=5001
|
- FLASK_RUN_PORT=5000
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- FLASK_DEBUG=1
|
- FLASK_DEBUG=1
|
||||||
- POSTGRES_HOST=db
|
- POSTGRES_HOST=db
|
||||||
|
@ -18,6 +18,10 @@ services:
|
||||||
- POSTGRES_USER=ploughshares
|
- POSTGRES_USER=ploughshares
|
||||||
- POSTGRES_PASSWORD=ploughshares_password
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
- APP_ENV=development
|
- 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:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
@ -25,7 +25,7 @@ COPY static/ ./static/
|
||||||
# Create uploads directory
|
# Create uploads directory
|
||||||
RUN mkdir -p uploads
|
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..." && \
|
RUN echo "Calculating CSP hashes for static resources..." && \
|
||||||
# Calculate hash for JS files
|
# Calculate hash for JS files
|
||||||
JS_HASH=$(openssl dgst -sha256 -binary static/js/bootstrap.bundle.min.js | openssl base64) && \
|
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" && \
|
echo "CSS hash: $CSS_HASH" && \
|
||||||
CUSTOM_CSS_HASH=$(openssl dgst -sha256 -binary static/css/custom.css | openssl base64) && \
|
CUSTOM_CSS_HASH=$(openssl dgst -sha256 -binary static/css/custom.css | openssl base64) && \
|
||||||
echo "Custom CSS hash: $CUSTOM_CSS_HASH" && \
|
echo "Custom CSS hash: $CUSTOM_CSS_HASH" && \
|
||||||
# Create environment variables file for CSP hashes
|
# Export CSP hashes as environment variables directly
|
||||||
echo "export CSP_JS_HASH=$JS_HASH" > /app/csp_hashes.env && \
|
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_CSS_HASH=\"$CSS_HASH\"" >> /app/csp_hashes.env && \
|
||||||
echo "export CSP_CUSTOM_CSS_HASH=$CUSTOM_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
|
# Set build argument for APP_VERSION
|
||||||
ARG APP_VERSION=unknown
|
ARG APP_VERSION=unknown
|
||||||
ENV APP_VERSION=$APP_VERSION
|
ENV APP_VERSION=$APP_VERSION
|
||||||
|
|
||||||
# Expose the port the app runs on
|
# Expose the port the app runs on
|
||||||
EXPOSE 5001
|
EXPOSE 5000
|
||||||
|
|
||||||
# Command to run the application with CSP hashes
|
# Command to run the application with CSP hashes
|
||||||
CMD ["/bin/bash", "-c", "source /app/csp_hashes.env && exec python app.py"]
|
CMD ["/bin/bash", "-c", "source /app/csp_hashes.env && exec python app.py"]
|
|
@ -7,6 +7,7 @@ from datetime import datetime
|
||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
|
import re
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -53,10 +54,9 @@ if CSP_CSS_HASH:
|
||||||
if CSP_CUSTOM_CSS_HASH:
|
if CSP_CUSTOM_CSS_HASH:
|
||||||
logger.info(f"Using pre-calculated CSP hash for custom CSS: sha256-{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
|
# Define the base CSP for UI routes
|
||||||
# Base CSP settings with pre-calculated hashes for static resources
|
ui_csp = {
|
||||||
csp = {
|
'default-src': "'none'", # Deny by default
|
||||||
'default-src': ["'self'"],
|
|
||||||
'script-src': ["'self'"] + ([f"'sha256-{CSP_JS_HASH}'"] if CSP_JS_HASH else []),
|
'script-src': ["'self'"] + ([f"'sha256-{CSP_JS_HASH}'"] if CSP_JS_HASH else []),
|
||||||
'style-src': ["'self'"] +
|
'style-src': ["'self'"] +
|
||||||
([f"'sha256-{CSP_CSS_HASH}'"] if CSP_CSS_HASH else []) +
|
([f"'sha256-{CSP_CSS_HASH}'"] if CSP_CSS_HASH else []) +
|
||||||
|
@ -64,8 +64,9 @@ csp = {
|
||||||
'img-src': ["'self'", "data:"],
|
'img-src': ["'self'", "data:"],
|
||||||
'font-src': ["'self'"],
|
'font-src': ["'self'"],
|
||||||
'connect-src': "'self'",
|
'connect-src': "'self'",
|
||||||
'object-src': "'none'",
|
'manifest-src': "'self'",
|
||||||
'frame-ancestors': "'none'",
|
'object-src': "'none'", # Explicitly disallow objects
|
||||||
|
'frame-ancestors': "'none'", # Prevent framing
|
||||||
'base-uri': "'self'",
|
'base-uri': "'self'",
|
||||||
'form-action': "'self'"
|
'form-action': "'self'"
|
||||||
}
|
}
|
||||||
|
@ -74,18 +75,18 @@ csp = {
|
||||||
if APP_DOMAIN:
|
if APP_DOMAIN:
|
||||||
logger.info(f"Configuring CSP for domain: {APP_DOMAIN}")
|
logger.info(f"Configuring CSP for domain: {APP_DOMAIN}")
|
||||||
# Add domain to connect-src if needed
|
# Add domain to connect-src if needed
|
||||||
if APP_DOMAIN not in csp['connect-src']:
|
if APP_DOMAIN not in ui_csp['connect-src']:
|
||||||
if isinstance(csp['connect-src'], list):
|
if isinstance(ui_csp['connect-src'], list):
|
||||||
csp['connect-src'].append(APP_DOMAIN)
|
ui_csp['connect-src'].append(APP_DOMAIN)
|
||||||
else:
|
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
|
# Update form-action to include the domain
|
||||||
if isinstance(csp['form-action'], list):
|
if isinstance(ui_csp['form-action'], list):
|
||||||
if APP_DOMAIN not in csp['form-action']:
|
if APP_DOMAIN not in ui_csp['form-action']:
|
||||||
csp['form-action'].append(APP_DOMAIN)
|
ui_csp['form-action'].append(APP_DOMAIN)
|
||||||
else:
|
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)
|
# Configure Permissions-Policy (formerly Feature-Policy)
|
||||||
# Deny access to all browser features that we don't need
|
# Deny access to all browser features that we don't need
|
||||||
|
@ -131,10 +132,17 @@ additional_headers = {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
'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(
|
talisman = Talisman(
|
||||||
app,
|
app,
|
||||||
content_security_policy=csp,
|
content_security_policy=csp_for_request,
|
||||||
content_security_policy_nonce_in=['script-src'],
|
content_security_policy_nonce_in=['script-src'],
|
||||||
feature_policy=permissions_policy,
|
feature_policy=permissions_policy,
|
||||||
force_https=force_https,
|
force_https=force_https,
|
||||||
|
@ -149,13 +157,44 @@ talisman = Talisman(
|
||||||
session_cookie_http_only=True
|
session_cookie_http_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add additional security headers that Talisman doesn't support natively
|
# Add CORS headers for API routes
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(response):
|
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():
|
for header, value in additional_headers.items():
|
||||||
response.headers[header] = value
|
response.headers[header] = value
|
||||||
|
|
||||||
return response
|
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
|
# Set locale for currency formatting
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||||
|
|
Loading…
Reference in New Issue