security_headers.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import re
  2. import os
  3. from fastapi import Request
  4. from starlette.middleware.base import BaseHTTPMiddleware
  5. from typing import Dict
  6. class SecurityHeadersMiddleware(BaseHTTPMiddleware):
  7. async def dispatch(self, request: Request, call_next):
  8. response = await call_next(request)
  9. response.headers.update(set_security_headers())
  10. return response
  11. def set_security_headers() -> Dict[str, str]:
  12. """
  13. Sets security headers based on environment variables.
  14. This function reads specific environment variables and uses their values
  15. to set corresponding security headers. The headers that can be set are:
  16. - cache-control
  17. - permissions-policy
  18. - strict-transport-security
  19. - referrer-policy
  20. - x-content-type-options
  21. - x-download-options
  22. - x-frame-options
  23. - x-permitted-cross-domain-policies
  24. Each environment variable is associated with a specific setter function
  25. that constructs the header. If the environment variable is set, the
  26. corresponding header is added to the options dictionary.
  27. Returns:
  28. dict: A dictionary containing the security headers and their values.
  29. """
  30. options = {}
  31. header_setters = {
  32. "CACHE_CONTROL": set_cache_control,
  33. "HSTS": set_hsts,
  34. "PERMISSIONS_POLICY": set_permissions_policy,
  35. "REFERRER_POLICY": set_referrer,
  36. "XCONTENT_TYPE": set_xcontent_type,
  37. "XDOWNLOAD_OPTIONS": set_xdownload_options,
  38. "XFRAME_OPTIONS": set_xframe,
  39. "XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
  40. }
  41. for env_var, setter in header_setters.items():
  42. value = os.environ.get(env_var, None)
  43. if value:
  44. header = setter(value)
  45. if header:
  46. options.update(header)
  47. return options
  48. # Set HTTP Strict Transport Security(HSTS) response header
  49. def set_hsts(value: str):
  50. pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
  51. match = re.match(pattern, value, re.IGNORECASE)
  52. if not match:
  53. value = "max-age=31536000;includeSubDomains"
  54. return {"Strict-Transport-Security": value}
  55. # Set X-Frame-Options response header
  56. def set_xframe(value: str):
  57. pattern = r"^(DENY|SAMEORIGIN)$"
  58. match = re.match(pattern, value, re.IGNORECASE)
  59. if not match:
  60. value = "DENY"
  61. return {"X-Frame-Options": value}
  62. # Set Permissions-Policy response header
  63. def set_permissions_policy(value: str):
  64. pattern = r"^(?:(accelerometer|autoplay|camera|clipboard-read|clipboard-write|fullscreen|geolocation|gyroscope|magnetometer|microphone|midi|payment|picture-in-picture|sync-xhr|usb|xr-spatial-tracking)=\((self)?\),?)*$"
  65. match = re.match(pattern, value, re.IGNORECASE)
  66. if not match:
  67. value = "none"
  68. return {"Permissions-Policy": value}
  69. # Set Referrer-Policy response header
  70. def set_referrer(value: str):
  71. pattern = r"^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$"
  72. match = re.match(pattern, value, re.IGNORECASE)
  73. if not match:
  74. value = "no-referrer"
  75. return {"Referrer-Policy": value}
  76. # Set Cache-Control response header
  77. def set_cache_control(value: str):
  78. pattern = r"^(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable)(,\s*(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable))*$"
  79. match = re.match(pattern, value, re.IGNORECASE)
  80. if not match:
  81. value = "no-store, max-age=0"
  82. return {"Cache-Control": value}
  83. # Set X-Download-Options response header
  84. def set_xdownload_options(value: str):
  85. if value != "noopen":
  86. value = "noopen"
  87. return {"X-Download-Options": value}
  88. # Set X-Content-Type-Options response header
  89. def set_xcontent_type(value: str):
  90. if value != "nosniff":
  91. value = "nosniff"
  92. return {"X-Content-Type-Options": value}
  93. # Set X-Permitted-Cross-Domain-Policies response header
  94. def set_xpermitted_cross_domain_policies(value: str):
  95. pattern = r"^(none|master-only|by-content-type|by-ftp-filename)$"
  96. match = re.match(pattern, value, re.IGNORECASE)
  97. if not match:
  98. value = "none"
  99. return {"X-Permitted-Cross-Domain-Policies": value}