05/09 Update: security enhanced
This commit is contained in:
+33
-9
@@ -1,4 +1,4 @@
|
||||
from flask import Flask, render_template
|
||||
from flask import Flask, render_template, request as _flask_request
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
@@ -6,6 +6,7 @@ from flask_wtf.csrf import CSRFProtect
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
from flask_cors import CORS
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from .config import config
|
||||
|
||||
@@ -15,6 +16,16 @@ login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
def client_ip() -> str:
|
||||
"""
|
||||
Return the real client IP address, trusted only from one upstream proxy
|
||||
(Nginx). ProxyFix — applied in create_app() — ensures request.remote_addr
|
||||
is already set to the correct value; we do NOT parse X-Forwarded-For
|
||||
manually here, which would be spoofable.
|
||||
"""
|
||||
return _flask_request.remote_addr or ''
|
||||
|
||||
# APScheduler is used for the background token-blacklist cleanup job.
|
||||
# Imported here so it is available at module level; started inside create_app().
|
||||
try:
|
||||
@@ -26,6 +37,11 @@ except ImportError: # pragma: no cover — optional dependency
|
||||
|
||||
def create_app(config_name: str = 'development') -> Flask:
|
||||
app = Flask(__name__)
|
||||
# Trust exactly one upstream proxy (Nginx) for X-Forwarded-For / X-Forwarded-Proto.
|
||||
# x_for=1 means only the rightmost hop in XFF is trusted, making rate-limit
|
||||
# IP keys spoof-resistant — a client cannot bypass per-IP limits by injecting
|
||||
# an arbitrary IP into the XFF header.
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Extensions
|
||||
@@ -46,21 +62,27 @@ def create_app(config_name: str = 'development') -> Flask:
|
||||
# Attach security headers to every response
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
# Strict-Transport-Security: enforce HTTPS for 1 year, include subdomains
|
||||
# HSTS: enforce HTTPS for 1 year across all subdomains.
|
||||
# 'preload' enables browser preload-list submission so first-time
|
||||
# HTTP visitors are also protected before the first redirect.
|
||||
response.headers['Strict-Transport-Security'] = (
|
||||
'max-age=31536000; includeSubDomains'
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
)
|
||||
# Prevent clickjacking
|
||||
# Prevent clickjacking (belt-and-suspenders alongside CSP frame-ancestors)
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
# Prevent MIME-type sniffing
|
||||
# Prevent MIME-type sniffing attacks
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
# Control referrer information leakage
|
||||
# Limit referrer information sent on navigation
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
# Permissions policy — disable features the app does not use
|
||||
# Permissions policy — disable browser features the app does not use
|
||||
response.headers['Permissions-Policy'] = (
|
||||
'geolocation=(), camera=(), microphone=()'
|
||||
)
|
||||
# CSP via HTTP header (authoritative — overrides the meta tag for all resources)
|
||||
# Content-Security-Policy (HTTP header is authoritative — overrides meta tag).
|
||||
# base-uri 'self' — blocks <base href> injection that would redirect all
|
||||
# relative URLs to an attacker-controlled origin.
|
||||
# upgrade-insecure-requests — instructs browsers to rewrite http:// sub-resource
|
||||
# requests to https:// to avoid mixed-content warnings.
|
||||
response.headers['Content-Security-Policy'] = (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self'; "
|
||||
@@ -68,7 +90,9 @@ def create_app(config_name: str = 'development') -> Flask:
|
||||
"img-src 'self' data:; "
|
||||
"font-src 'self'; "
|
||||
"connect-src 'self' https://api.pwnedpasswords.com; "
|
||||
"frame-ancestors 'none';"
|
||||
"frame-ancestors 'none'; "
|
||||
"base-uri 'self'; "
|
||||
"upgrade-insecure-requests;"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
Reference in New Issue
Block a user