security_headers.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. - content-security-policy
  25. Each environment variable is associated with a specific setter function
  26. that constructs the header. If the environment variable is set, the
  27. corresponding header is added to the options dictionary.
  28. Returns:
  29. dict: A dictionary containing the security headers and their values.
  30. """
  31. options = {}
  32. header_setters = {
  33. "CACHE_CONTROL": set_cache_control,
  34. "HSTS": set_hsts,
  35. "PERMISSIONS_POLICY": set_permissions_policy,
  36. "REFERRER_POLICY": set_referrer,
  37. "XCONTENT_TYPE": set_xcontent_type,
  38. "XDOWNLOAD_OPTIONS": set_xdownload_options,
  39. "XFRAME_OPTIONS": set_xframe,
  40. "XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
  41. "CONTENT_SECURITY_POLICY": set_content_security_policy,
  42. }
  43. for env_var, setter in header_setters.items():
  44. value = os.environ.get(env_var, None)
  45. if value:
  46. header = setter(value)
  47. if header:
  48. options.update(header)
  49. return options
  50. # Set HTTP Strict Transport Security(HSTS) response header
  51. def set_hsts(value: str):
  52. pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
  53. match = re.match(pattern, value, re.IGNORECASE)
  54. if not match:
  55. value = "max-age=31536000;includeSubDomains"
  56. return {"Strict-Transport-Security": value}
  57. # Set X-Frame-Options response header
  58. def set_xframe(value: str):
  59. pattern = r"^(DENY|SAMEORIGIN)$"
  60. match = re.match(pattern, value, re.IGNORECASE)
  61. if not match:
  62. value = "DENY"
  63. return {"X-Frame-Options": value}
  64. # Set Permissions-Policy response header
  65. def set_permissions_policy(value: str):
  66. 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)?\),?)*$"
  67. match = re.match(pattern, value, re.IGNORECASE)
  68. if not match:
  69. value = "none"
  70. return {"Permissions-Policy": value}
  71. # Set Referrer-Policy response header
  72. def set_referrer(value: str):
  73. pattern = r"^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$"
  74. match = re.match(pattern, value, re.IGNORECASE)
  75. if not match:
  76. value = "no-referrer"
  77. return {"Referrer-Policy": value}
  78. # Set Cache-Control response header
  79. def set_cache_control(value: str):
  80. 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))*$"
  81. match = re.match(pattern, value, re.IGNORECASE)
  82. if not match:
  83. value = "no-store, max-age=0"
  84. return {"Cache-Control": value}
  85. # Set X-Download-Options response header
  86. def set_xdownload_options(value: str):
  87. if value != "noopen":
  88. value = "noopen"
  89. return {"X-Download-Options": value}
  90. # Set X-Content-Type-Options response header
  91. def set_xcontent_type(value: str):
  92. if value != "nosniff":
  93. value = "nosniff"
  94. return {"X-Content-Type-Options": value}
  95. # Set X-Permitted-Cross-Domain-Policies response header
  96. def set_xpermitted_cross_domain_policies(value: str):
  97. pattern = r"^(none|master-only|by-content-type|by-ftp-filename)$"
  98. match = re.match(pattern, value, re.IGNORECASE)
  99. if not match:
  100. value = "none"
  101. return {"X-Permitted-Cross-Domain-Policies": value}
  102. # Set Content-Security-Policy response header
  103. def set_content_security_policy(value: str):
  104. return {"Content-Security-Policy": value}