|
152 | 152 | # Initialize JWT Manager |
153 | 153 | jwt = JWTManager(app) |
154 | 154 |
|
| 155 | +DEFAULT_CORS_ORIGINS = [ |
| 156 | + "http://127.0.0.1:5500", |
| 157 | + "http://localhost:5500", |
| 158 | + "http://127.0.0.1:5501", |
| 159 | + "http://localhost:5501", |
| 160 | + "http://127.0.0.1:5000", |
| 161 | + "http://localhost:5000", |
| 162 | +] |
| 163 | +ALLOWED_CORS_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] |
| 164 | +ALLOWED_CORS_HEADERS = [ |
| 165 | + "Content-Type", |
| 166 | + "Authorization", |
| 167 | + "X-CSRF-TOKEN", |
| 168 | + "X-CSRF-Token", |
| 169 | + "X-Requested-With", |
| 170 | + "Accept", |
| 171 | +] |
| 172 | + |
| 173 | +def _load_cors_origins() -> list[str]: |
| 174 | + """Load an explicit CORS allowlist from the environment or defaults.""" |
| 175 | + raw_origins = os.getenv("ALLOWED_ORIGINS", "") |
| 176 | + if raw_origins.strip(): |
| 177 | + return [origin.strip() for origin in raw_origins.split(",") if origin.strip()] |
| 178 | + return DEFAULT_CORS_ORIGINS |
| 179 | + |
| 180 | +def _apply_security_headers(response): |
| 181 | + """Apply hardening headers to every API response.""" |
| 182 | + csp_policy = ( |
| 183 | + "default-src 'self'; " |
| 184 | + "script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; " |
| 185 | + "style-src 'self' https://fonts.googleapis.com https://cdnjs.cloudflare.com; " |
| 186 | + "img-src 'self' data: https:; " |
| 187 | + "font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; " |
| 188 | + "connect-src 'self' https: ws: wss:; " |
| 189 | + "object-src 'none'; " |
| 190 | + "base-uri 'self'; " |
| 191 | + "frame-ancestors 'none'; " |
| 192 | + "form-action 'self'; " |
| 193 | + "upgrade-insecure-requests" |
| 194 | + ) |
| 195 | + |
| 196 | + response.headers['Content-Security-Policy'] = csp_policy |
| 197 | + response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' |
| 198 | + response.headers['X-Content-Type-Options'] = 'nosniff' |
| 199 | + response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' |
| 200 | + response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' |
| 201 | + response.headers['X-Frame-Options'] = 'DENY' |
| 202 | + return response |
| 203 | + |
155 | 204 | # ===================================================================== |
156 | 205 | # SECURITY COMPLIANCE UPDATE: CORS CONFIGURATION |
157 | 206 | # The previous CORS(app) was overly permissive and allowed all origins. |
|
162 | 211 | # optionally load allowed origins from environment variables. |
163 | 212 | # ===================================================================== |
164 | 213 | # ALLOWED_ORIGINS=http://127.0.0.1:5500,http://localhost:5500,http://127.0.0.1:5000,http://localhost:5000 |
165 | | -# For development, we'll allow all to be safe, then restrict in prod |
166 | | -CORS(app, supports_credentials=True, origins=["http://127.0.0.1:5500", "http://localhost:5500", "http://127.0.0.1:5501", "http://localhost:5501", "http://127.0.0.1:5000", "http://localhost:5000"]) |
| 214 | +# For development, we keep a strict explicit allowlist and never use '*' |
| 215 | +_cors_origins = _load_cors_origins() |
| 216 | +CORS( |
| 217 | + app, |
| 218 | + supports_credentials=True, |
| 219 | + origins=_cors_origins, |
| 220 | + methods=ALLOWED_CORS_METHODS, |
| 221 | + allow_headers=ALLOWED_CORS_HEADERS, |
| 222 | + expose_headers=['Retry-After'], |
| 223 | + resources={r"/api/*": {"origins": _cors_origins}}, |
| 224 | +) |
167 | 225 |
|
168 | 226 | # Initialize cache service |
169 | 227 | cache_service.init_app(app) |
@@ -557,35 +615,7 @@ def add_security_headers(response): |
557 | 615 | Returns: |
558 | 616 | response: Response with added security headers |
559 | 617 | """ |
560 | | - csp_policy = ( |
561 | | - "default-src 'self'; " |
562 | | - "script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; " |
563 | | - "style-src 'self' https://fonts.googleapis.com https://cdnjs.cloudflare.com; " |
564 | | - "img-src 'self' data: blob: https:; " |
565 | | - "font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; " |
566 | | - "connect-src 'self' ws: wss: https:; " |
567 | | - "frame-ancestors 'none'; " |
568 | | - "base-uri 'self'; " |
569 | | - "form-action 'self'; " |
570 | | - "upgrade-insecure-requests" |
571 | | - ) |
572 | | - response.headers['Content-Security-Policy'] = csp_policy |
573 | | - response.headers['X-Content-Type-Options'] = 'nosniff' |
574 | | - response.headers['X-Frame-Options'] = 'DENY' |
575 | | - response.headers['X-XSS-Protection'] = '1; mode=block' |
576 | | - response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' |
577 | | - response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' |
578 | | - response.headers['Permissions-Policy'] = ( |
579 | | - 'geolocation=(), ' |
580 | | - 'microphone=(), ' |
581 | | - 'camera=(), ' |
582 | | - 'payment=(), ' |
583 | | - 'usb=(), ' |
584 | | - 'magnetometer=(), ' |
585 | | - 'gyroscope=(), ' |
586 | | - 'accelerometer=()' |
587 | | - ) |
588 | | - return response |
| 618 | + return _apply_security_headers(response) |
589 | 619 |
|
590 | 620 | # Rate limiting configuration |
591 | 621 | RATE_LIMIT_WINDOW = int(os.getenv('RATE_LIMIT_WINDOW', '60')) |
|
0 commit comments