checks.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from __future__ import annotations
  2. import re
  3. from collections.abc import Sequence
  4. from typing import Any
  5. from urllib.parse import urlsplit
  6. from django.conf import settings
  7. from django.core.checks import CheckMessage
  8. from django.core.checks import Error
  9. from corsheaders.conf import conf
  10. re_type = type(re.compile(""))
  11. def check_settings(**kwargs: Any) -> list[CheckMessage]:
  12. errors: list[CheckMessage] = []
  13. if not is_sequence(conf.CORS_ALLOW_HEADERS, str):
  14. errors.append(
  15. Error(
  16. "CORS_ALLOW_HEADERS should be a sequence of strings.",
  17. id="corsheaders.E001",
  18. )
  19. )
  20. if not is_sequence(conf.CORS_ALLOW_METHODS, str):
  21. errors.append(
  22. Error(
  23. "CORS_ALLOW_METHODS should be a sequence of strings.",
  24. id="corsheaders.E002",
  25. )
  26. )
  27. if not isinstance(conf.CORS_ALLOW_CREDENTIALS, bool):
  28. errors.append( # type: ignore [unreachable]
  29. Error("CORS_ALLOW_CREDENTIALS should be a bool.", id="corsheaders.E003")
  30. )
  31. if not isinstance(conf.CORS_ALLOW_PRIVATE_NETWORK, bool):
  32. errors.append( # type: ignore [unreachable]
  33. Error(
  34. "CORS_ALLOW_PRIVATE_NETWORK should be a bool.",
  35. id="corsheaders.E015",
  36. )
  37. )
  38. if (
  39. not isinstance(conf.CORS_PREFLIGHT_MAX_AGE, int)
  40. or conf.CORS_PREFLIGHT_MAX_AGE < 0
  41. ):
  42. errors.append(
  43. Error(
  44. (
  45. "CORS_PREFLIGHT_MAX_AGE should be an integer greater than "
  46. + "or equal to zero."
  47. ),
  48. id="corsheaders.E004",
  49. )
  50. )
  51. if not isinstance(conf.CORS_ALLOW_ALL_ORIGINS, bool):
  52. if hasattr(settings, "CORS_ALLOW_ALL_ORIGINS"): # type: ignore [unreachable]
  53. allow_all_alias = "CORS_ALLOW_ALL_ORIGINS"
  54. else:
  55. allow_all_alias = "CORS_ORIGIN_ALLOW_ALL"
  56. errors.append(
  57. Error(
  58. f"{allow_all_alias} should be a bool.",
  59. id="corsheaders.E005",
  60. )
  61. )
  62. if hasattr(settings, "CORS_ALLOWED_ORIGINS"):
  63. allowed_origins_alias = "CORS_ALLOWED_ORIGINS"
  64. else:
  65. allowed_origins_alias = "CORS_ORIGIN_WHITELIST"
  66. if not is_sequence(conf.CORS_ALLOWED_ORIGINS, str):
  67. errors.append(
  68. Error(
  69. f"{allowed_origins_alias} should be a sequence of strings.",
  70. id="corsheaders.E006",
  71. )
  72. )
  73. else:
  74. special_origin_values = (
  75. # From 'security sensitive' contexts
  76. "null",
  77. # From files on Chrome on Android
  78. # https://bugs.chromium.org/p/chromium/issues/detail?id=991107
  79. "file://",
  80. )
  81. for origin in conf.CORS_ALLOWED_ORIGINS:
  82. if origin in special_origin_values:
  83. continue
  84. parsed = urlsplit(origin)
  85. if parsed.scheme == "" or parsed.netloc == "":
  86. errors.append(
  87. Error(
  88. "Origin {} in {} is missing scheme or netloc".format(
  89. repr(origin), allowed_origins_alias
  90. ),
  91. id="corsheaders.E013",
  92. hint=(
  93. "Add a scheme (e.g. https://) or netloc (e.g. "
  94. + "example.com)."
  95. ),
  96. )
  97. )
  98. else:
  99. # Only do this check in this case because if the scheme is not
  100. # provided, netloc ends up in path
  101. for part in ("path", "query", "fragment"):
  102. if getattr(parsed, part) != "":
  103. errors.append(
  104. Error(
  105. "Origin {} in {} should not have {}".format(
  106. repr(origin), allowed_origins_alias, part
  107. ),
  108. id="corsheaders.E014",
  109. )
  110. )
  111. if hasattr(settings, "CORS_ALLOWED_ORIGIN_REGEXES"):
  112. allowed_regexes_alias = "CORS_ALLOWED_ORIGIN_REGEXES"
  113. else:
  114. allowed_regexes_alias = "CORS_ORIGIN_REGEX_WHITELIST"
  115. if not is_sequence(conf.CORS_ALLOWED_ORIGIN_REGEXES, (str, re_type)):
  116. errors.append(
  117. Error(
  118. "{} should be a sequence of strings and/or compiled regexes.".format(
  119. allowed_regexes_alias
  120. ),
  121. id="corsheaders.E007",
  122. )
  123. )
  124. if not is_sequence(conf.CORS_EXPOSE_HEADERS, str):
  125. errors.append(
  126. Error("CORS_EXPOSE_HEADERS should be a sequence.", id="corsheaders.E008")
  127. )
  128. if not isinstance(conf.CORS_URLS_REGEX, (str, re_type)):
  129. errors.append(
  130. Error("CORS_URLS_REGEX should be a string or regex.", id="corsheaders.E009")
  131. )
  132. if hasattr(settings, "CORS_MODEL"):
  133. errors.append(
  134. Error(
  135. (
  136. "The CORS_MODEL setting has been removed - see "
  137. + "django-cors-headers' HISTORY."
  138. ),
  139. id="corsheaders.E012",
  140. )
  141. )
  142. if hasattr(settings, "CORS_REPLACE_HTTPS_REFERER"):
  143. errors.append(
  144. Error(
  145. (
  146. "The CORS_REPLACE_HTTPS_REFERER setting has been removed"
  147. + " - see django-cors-headers' CHANGELOG."
  148. ),
  149. id="corsheaders.E013",
  150. )
  151. )
  152. return errors
  153. def is_sequence(thing: Any, type_or_types: type[Any] | tuple[type[Any], ...]) -> bool:
  154. return isinstance(thing, Sequence) and all(
  155. isinstance(x, type_or_types) for x in thing
  156. )