plain 0.4.1__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/cli/cli.py +36 -13
- plain/csrf/middleware.py +23 -40
- plain/forms/fields.py +2 -3
- plain/forms/forms.py +2 -1
- plain/http/request.py +5 -5
- plain/internal/handlers/base.py +13 -1
- plain/internal/middleware/headers.py +19 -0
- plain/internal/middleware/https.py +36 -0
- plain/{middleware/common.py → internal/middleware/slash.py} +2 -25
- plain/preflight/security/base.py +5 -174
- plain/preflight/security/csrf.py +1 -5
- plain/runtime/README.md +0 -4
- plain/runtime/__init__.py +13 -15
- plain/runtime/global_settings.py +38 -55
- plain/runtime/user_settings.py +226 -217
- plain/signing.py +5 -23
- plain/test/client.py +17 -17
- plain/utils/timezone.py +2 -23
- plain/views/base.py +4 -0
- {plain-0.4.1.dist-info → plain-0.11.0.dist-info}/METADATA +2 -2
- {plain-0.4.1.dist-info → plain-0.11.0.dist-info}/RECORD +25 -27
- {plain-0.4.1.dist-info → plain-0.11.0.dist-info}/WHEEL +1 -1
- plain/middleware/README.md +0 -3
- plain/middleware/clickjacking.py +0 -52
- plain/middleware/gzip.py +0 -64
- plain/middleware/security.py +0 -64
- /plain/{middleware → internal/middleware}/__init__.py +0 -0
- {plain-0.4.1.dist-info → plain-0.11.0.dist-info}/LICENSE +0 -0
- {plain-0.4.1.dist-info → plain-0.11.0.dist-info}/entry_points.txt +0 -0
plain/cli/cli.py
CHANGED
@@ -5,6 +5,7 @@ import shutil
|
|
5
5
|
import subprocess
|
6
6
|
import sys
|
7
7
|
import traceback
|
8
|
+
from importlib.metadata import entry_points
|
8
9
|
from importlib.util import find_spec
|
9
10
|
from pathlib import Path
|
10
11
|
|
@@ -14,7 +15,9 @@ from click.core import Command, Context
|
|
14
15
|
import plain.runtime
|
15
16
|
from plain import preflight
|
16
17
|
from plain.assets.compile import compile_assets, get_compiled_path
|
18
|
+
from plain.exceptions import ImproperlyConfigured
|
17
19
|
from plain.packages import packages
|
20
|
+
from plain.utils.crypto import get_random_string
|
18
21
|
|
19
22
|
from .formatting import PlainContext
|
20
23
|
from .packages import EntryPointGroup, InstalledPackagesGroup
|
@@ -248,7 +251,7 @@ def preflight_checks(package_label, deploy, fail_level, databases):
|
|
248
251
|
msg = header + body + footer
|
249
252
|
click.echo(msg, err=True)
|
250
253
|
else:
|
251
|
-
click.
|
254
|
+
click.secho("✔ Preflight check identified no issues.", err=True, fg="green")
|
252
255
|
|
253
256
|
|
254
257
|
@plain_cli.command()
|
@@ -284,17 +287,10 @@ def compile(keep_original, fingerprint, compress):
|
|
284
287
|
)
|
285
288
|
sys.exit(1)
|
286
289
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
click.secho("Compiling Tailwind CSS", bold=True)
|
291
|
-
result = subprocess.run(["plain", "tailwind", "compile", "--minify"])
|
290
|
+
for entry_point in entry_points(group="plain.assets.compile"):
|
291
|
+
click.secho(f"Running {entry_point.name}", bold=True)
|
292
|
+
result = entry_point.load()()
|
292
293
|
print()
|
293
|
-
if result.returncode:
|
294
|
-
click.secho(
|
295
|
-
f"Error compiling Tailwind CSS (exit {result.returncode})", fg="red"
|
296
|
-
)
|
297
|
-
sys.exit(result.returncode)
|
298
294
|
|
299
295
|
# TODO also look in [tool.plain.compile.run]
|
300
296
|
|
@@ -406,6 +402,18 @@ def setting(setting_name):
|
|
406
402
|
click.secho(f'Setting "{setting_name}" not found', fg="red")
|
407
403
|
|
408
404
|
|
405
|
+
@plain_cli.group()
|
406
|
+
def utils():
|
407
|
+
pass
|
408
|
+
|
409
|
+
|
410
|
+
@utils.command()
|
411
|
+
def generate_secret_key():
|
412
|
+
"""Generate a new secret key"""
|
413
|
+
new_secret_key = get_random_string(50)
|
414
|
+
click.echo(new_secret_key)
|
415
|
+
|
416
|
+
|
409
417
|
class AppCLIGroup(click.Group):
|
410
418
|
"""
|
411
419
|
Loads app.cli if it exists as `plain app`
|
@@ -459,16 +467,31 @@ class PlainCommandCollection(click.CommandCollection):
|
|
459
467
|
EntryPointGroup(),
|
460
468
|
plain_cli,
|
461
469
|
]
|
462
|
-
except
|
470
|
+
except ImproperlyConfigured as e:
|
463
471
|
click.secho(
|
464
|
-
|
472
|
+
str(e),
|
465
473
|
fg="red",
|
466
474
|
err=True,
|
467
475
|
)
|
476
|
+
|
477
|
+
# None of these require the app to be setup
|
478
|
+
sources = [
|
479
|
+
EntryPointGroup(),
|
480
|
+
AppCLIGroup(),
|
481
|
+
plain_cli,
|
482
|
+
]
|
483
|
+
except Exception as e:
|
468
484
|
print("---")
|
469
485
|
print(traceback.format_exc())
|
470
486
|
print("---")
|
471
487
|
|
488
|
+
click.secho(
|
489
|
+
f"Error: {e}",
|
490
|
+
fg="red",
|
491
|
+
err=True,
|
492
|
+
)
|
493
|
+
|
494
|
+
# None of these require the app to be setup
|
472
495
|
sources = [
|
473
496
|
EntryPointGroup(),
|
474
497
|
AppCLIGroup(),
|
plain/csrf/middleware.py
CHANGED
@@ -9,7 +9,7 @@ import string
|
|
9
9
|
from collections import defaultdict
|
10
10
|
from urllib.parse import urlparse
|
11
11
|
|
12
|
-
from plain.exceptions import DisallowedHost
|
12
|
+
from plain.exceptions import DisallowedHost
|
13
13
|
from plain.http import HttpHeaders, UnreadablePostError
|
14
14
|
from plain.logs import log_response
|
15
15
|
from plain.runtime import settings
|
@@ -242,44 +242,31 @@ class CsrfViewMiddleware:
|
|
242
242
|
If the CSRF_USE_SESSIONS setting is false, raises InvalidTokenFormat if
|
243
243
|
the request's secret has invalid characters or an invalid length.
|
244
244
|
"""
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
raise ImproperlyConfigured(
|
250
|
-
"CSRF_USE_SESSIONS is enabled, but request.session is not "
|
251
|
-
"set. SessionMiddleware must appear before CsrfViewMiddleware "
|
252
|
-
"in MIDDLEWARE."
|
253
|
-
)
|
245
|
+
try:
|
246
|
+
csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
|
247
|
+
except KeyError:
|
248
|
+
csrf_secret = None
|
254
249
|
else:
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
csrf_secret = None
|
259
|
-
else:
|
260
|
-
# This can raise InvalidTokenFormat.
|
261
|
-
_check_token_format(csrf_secret)
|
250
|
+
# This can raise InvalidTokenFormat.
|
251
|
+
_check_token_format(csrf_secret)
|
252
|
+
|
262
253
|
if csrf_secret is None:
|
263
254
|
return None
|
264
255
|
return csrf_secret
|
265
256
|
|
266
257
|
def _set_csrf_cookie(self, request, response):
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
samesite=settings.CSRF_COOKIE_SAMESITE,
|
280
|
-
)
|
281
|
-
# Set the Vary header since content varies with the CSRF cookie.
|
282
|
-
patch_vary_headers(response, ("Cookie",))
|
258
|
+
response.set_cookie(
|
259
|
+
settings.CSRF_COOKIE_NAME,
|
260
|
+
request.META["CSRF_COOKIE"],
|
261
|
+
max_age=settings.CSRF_COOKIE_AGE,
|
262
|
+
domain=settings.CSRF_COOKIE_DOMAIN,
|
263
|
+
path=settings.CSRF_COOKIE_PATH,
|
264
|
+
secure=settings.CSRF_COOKIE_SECURE,
|
265
|
+
httponly=settings.CSRF_COOKIE_HTTPONLY,
|
266
|
+
samesite=settings.CSRF_COOKIE_SAMESITE,
|
267
|
+
)
|
268
|
+
# Set the Vary header since content varies with the CSRF cookie.
|
269
|
+
patch_vary_headers(response, ("Cookie",))
|
283
270
|
|
284
271
|
def _origin_verified(self, request):
|
285
272
|
request_origin = request.META["HTTP_ORIGIN"]
|
@@ -289,7 +276,7 @@ class CsrfViewMiddleware:
|
|
289
276
|
pass
|
290
277
|
else:
|
291
278
|
good_origin = "{}://{}".format(
|
292
|
-
"https" if request.
|
279
|
+
"https" if request.is_https() else "http",
|
293
280
|
good_host,
|
294
281
|
)
|
295
282
|
if request_origin == good_origin:
|
@@ -331,11 +318,7 @@ class CsrfViewMiddleware:
|
|
331
318
|
):
|
332
319
|
return
|
333
320
|
# Allow matching the configured cookie domain.
|
334
|
-
good_referer =
|
335
|
-
settings.SESSION_COOKIE_DOMAIN
|
336
|
-
if settings.CSRF_USE_SESSIONS
|
337
|
-
else settings.CSRF_COOKIE_DOMAIN
|
338
|
-
)
|
321
|
+
good_referer = settings.CSRF_COOKIE_DOMAIN
|
339
322
|
if good_referer is None:
|
340
323
|
# If no cookie domain is configured, allow matching the current
|
341
324
|
# host:port exactly if it's permitted by ALLOWED_HOSTS.
|
@@ -435,7 +418,7 @@ class CsrfViewMiddleware:
|
|
435
418
|
return self._reject(
|
436
419
|
request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
|
437
420
|
)
|
438
|
-
elif request.
|
421
|
+
elif request.is_https():
|
439
422
|
# If the Origin header wasn't provided, reject HTTPS requests if
|
440
423
|
# the Referer header doesn't match an allowed value.
|
441
424
|
#
|
plain/forms/fields.py
CHANGED
@@ -14,7 +14,6 @@ from urllib.parse import urlsplit, urlunsplit
|
|
14
14
|
|
15
15
|
from plain import validators
|
16
16
|
from plain.exceptions import ValidationError
|
17
|
-
from plain.runtime import settings
|
18
17
|
from plain.utils import timezone
|
19
18
|
from plain.utils.dateparse import parse_datetime, parse_duration
|
20
19
|
from plain.utils.duration import duration_string
|
@@ -1001,7 +1000,7 @@ def from_current_timezone(value):
|
|
1001
1000
|
When time zone support is enabled, convert naive datetimes
|
1002
1001
|
entered in the current time zone to aware datetimes.
|
1003
1002
|
"""
|
1004
|
-
if
|
1003
|
+
if value is not None and timezone.is_naive(value):
|
1005
1004
|
current_timezone = timezone.get_current_timezone()
|
1006
1005
|
try:
|
1007
1006
|
if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
|
@@ -1025,6 +1024,6 @@ def to_current_timezone(value):
|
|
1025
1024
|
When time zone support is enabled, convert aware datetimes
|
1026
1025
|
to naive datetimes in the current time zone for display.
|
1027
1026
|
"""
|
1028
|
-
if
|
1027
|
+
if value is not None and timezone.is_aware(value):
|
1029
1028
|
return timezone.make_naive(value)
|
1030
1029
|
return value
|
plain/forms/forms.py
CHANGED
@@ -203,7 +203,8 @@ class BaseForm:
|
|
203
203
|
self._errors[field].extend(error_list)
|
204
204
|
|
205
205
|
# The field had an error, so removed it from the final data
|
206
|
-
|
206
|
+
# (we use getattr here so errors can be added to uncleaned forms)
|
207
|
+
if field in getattr(self, "cleaned_data", {}):
|
207
208
|
del self.cleaned_data[field]
|
208
209
|
|
209
210
|
def full_clean(self):
|
plain/http/request.py
CHANGED
@@ -140,7 +140,7 @@ class HttpRequest:
|
|
140
140
|
# Reconstruct the host using the algorithm from PEP 333.
|
141
141
|
host = self.META["SERVER_NAME"]
|
142
142
|
server_port = self.get_port()
|
143
|
-
if server_port != ("443" if self.
|
143
|
+
if server_port != ("443" if self.is_https() else "80"):
|
144
144
|
host = f"{host}:{server_port}"
|
145
145
|
return host
|
146
146
|
|
@@ -267,12 +267,12 @@ class HttpRequest:
|
|
267
267
|
|
268
268
|
@property
|
269
269
|
def scheme(self):
|
270
|
-
if settings.
|
270
|
+
if settings.HTTPS_PROXY_HEADER:
|
271
271
|
try:
|
272
|
-
header, secure_value = settings.
|
272
|
+
header, secure_value = settings.HTTPS_PROXY_HEADER
|
273
273
|
except ValueError:
|
274
274
|
raise ImproperlyConfigured(
|
275
|
-
"The
|
275
|
+
"The HTTPS_PROXY_HEADER setting must be a tuple containing "
|
276
276
|
"two values."
|
277
277
|
)
|
278
278
|
header_value = self.META.get(header)
|
@@ -281,7 +281,7 @@ class HttpRequest:
|
|
281
281
|
return "https" if header_value.strip() == secure_value else "http"
|
282
282
|
return self._get_scheme()
|
283
283
|
|
284
|
-
def
|
284
|
+
def is_https(self):
|
285
285
|
return self.scheme == "https"
|
286
286
|
|
287
287
|
@property
|
plain/internal/handlers/base.py
CHANGED
@@ -13,6 +13,15 @@ from .exception import convert_exception_to_response
|
|
13
13
|
logger = logging.getLogger("plain.request")
|
14
14
|
|
15
15
|
|
16
|
+
# These middleware classes are always used by Plain.
|
17
|
+
BUILTIN_MIDDLEWARE = [
|
18
|
+
"plain.internal.middleware.headers.DefaultHeadersMiddleware",
|
19
|
+
"plain.internal.middleware.https.HttpsRedirectMiddleware",
|
20
|
+
"plain.internal.middleware.slash.RedirectSlashMiddleware",
|
21
|
+
"plain.csrf.middleware.CsrfViewMiddleware",
|
22
|
+
]
|
23
|
+
|
24
|
+
|
16
25
|
class BaseHandler:
|
17
26
|
_view_middleware = None
|
18
27
|
_middleware_chain = None
|
@@ -27,7 +36,10 @@ class BaseHandler:
|
|
27
36
|
|
28
37
|
get_response = self._get_response
|
29
38
|
handler = convert_exception_to_response(get_response)
|
30
|
-
|
39
|
+
|
40
|
+
middlewares = reversed(BUILTIN_MIDDLEWARE + settings.MIDDLEWARE)
|
41
|
+
|
42
|
+
for middleware_path in middlewares:
|
31
43
|
middleware = import_string(middleware_path)
|
32
44
|
mw_instance = middleware(handler)
|
33
45
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from plain.runtime import settings
|
2
|
+
|
3
|
+
|
4
|
+
class DefaultHeadersMiddleware:
|
5
|
+
def __init__(self, get_response):
|
6
|
+
self.get_response = get_response
|
7
|
+
|
8
|
+
def __call__(self, request):
|
9
|
+
response = self.get_response(request)
|
10
|
+
|
11
|
+
for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
|
12
|
+
response.headers.setdefault(header, value)
|
13
|
+
|
14
|
+
# Add the Content-Length header to non-streaming responses if not
|
15
|
+
# already set.
|
16
|
+
if not response.streaming and not response.has_header("Content-Length"):
|
17
|
+
response.headers["Content-Length"] = str(len(response.content))
|
18
|
+
|
19
|
+
return response
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
from plain.http import ResponsePermanentRedirect
|
4
|
+
from plain.runtime import settings
|
5
|
+
|
6
|
+
|
7
|
+
class HttpsRedirectMiddleware:
|
8
|
+
def __init__(self, get_response):
|
9
|
+
self.get_response = get_response
|
10
|
+
|
11
|
+
# Settings for https (compile regexes once)
|
12
|
+
self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
|
13
|
+
self.https_redirect_host = settings.HTTPS_REDIRECT_HOST
|
14
|
+
self.https_redirect_exempt = [
|
15
|
+
re.compile(r) for r in settings.HTTPS_REDIRECT_EXEMPT
|
16
|
+
]
|
17
|
+
|
18
|
+
def __call__(self, request):
|
19
|
+
"""
|
20
|
+
Rewrite the URL based on settings.APPEND_SLASH
|
21
|
+
"""
|
22
|
+
|
23
|
+
if redirect_response := self.maybe_https_redirect(request):
|
24
|
+
return redirect_response
|
25
|
+
|
26
|
+
return self.get_response(request)
|
27
|
+
|
28
|
+
def maybe_https_redirect(self, request):
|
29
|
+
path = request.path.lstrip("/")
|
30
|
+
if (
|
31
|
+
self.https_redirect_enabled
|
32
|
+
and not request.is_https()
|
33
|
+
and not any(pattern.search(path) for pattern in self.https_redirect_exempt)
|
34
|
+
):
|
35
|
+
host = self.https_redirect_host or request.get_host()
|
36
|
+
return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
|
@@ -4,25 +4,7 @@ from plain.urls import is_valid_path
|
|
4
4
|
from plain.utils.http import escape_leading_slashes
|
5
5
|
|
6
6
|
|
7
|
-
class
|
8
|
-
"""
|
9
|
-
"Common" middleware for taking care of some basic operations:
|
10
|
-
|
11
|
-
- URL rewriting: Based on the APPEND_SLASH setting,
|
12
|
-
append missing slashes.
|
13
|
-
|
14
|
-
- If APPEND_SLASH is set and the initial URL doesn't end with a
|
15
|
-
slash, and it is not found in urlpatterns, form a new URL by
|
16
|
-
appending a slash at the end. If this new URL is found in
|
17
|
-
urlpatterns, return an HTTP redirect to this new URL; otherwise
|
18
|
-
process the initial URL as usual.
|
19
|
-
|
20
|
-
This behavior can be customized by subclassing CommonMiddleware and
|
21
|
-
overriding the response_redirect_class attribute.
|
22
|
-
"""
|
23
|
-
|
24
|
-
response_redirect_class = ResponsePermanentRedirect
|
25
|
-
|
7
|
+
class RedirectSlashMiddleware:
|
26
8
|
def __init__(self, get_response):
|
27
9
|
self.get_response = get_response
|
28
10
|
|
@@ -40,12 +22,7 @@ class CommonMiddleware:
|
|
40
22
|
# If the given URL is "Not Found", then check if we should redirect to
|
41
23
|
# a path with a slash appended.
|
42
24
|
if response.status_code == 404 and self.should_redirect_with_slash(request):
|
43
|
-
return
|
44
|
-
|
45
|
-
# Add the Content-Length header to non-streaming responses if not
|
46
|
-
# already set.
|
47
|
-
if not response.streaming and not response.has_header("Content-Length"):
|
48
|
-
response.headers["Content-Length"] = str(len(response.content))
|
25
|
+
return ResponsePermanentRedirect(self.get_full_path_with_slash(request))
|
49
26
|
|
50
27
|
return response
|
51
28
|
|
plain/preflight/security/base.py
CHANGED
@@ -1,23 +1,7 @@
|
|
1
1
|
from plain.exceptions import ImproperlyConfigured
|
2
2
|
from plain.runtime import settings
|
3
3
|
|
4
|
-
from .. import
|
5
|
-
|
6
|
-
CROSS_ORIGIN_OPENER_POLICY_VALUES = {
|
7
|
-
"same-origin",
|
8
|
-
"same-origin-allow-popups",
|
9
|
-
"unsafe-none",
|
10
|
-
}
|
11
|
-
REFERRER_POLICY_VALUES = {
|
12
|
-
"no-referrer",
|
13
|
-
"no-referrer-when-downgrade",
|
14
|
-
"origin",
|
15
|
-
"origin-when-cross-origin",
|
16
|
-
"same-origin",
|
17
|
-
"strict-origin",
|
18
|
-
"strict-origin-when-cross-origin",
|
19
|
-
"unsafe-url",
|
20
|
-
}
|
4
|
+
from .. import Warning, register
|
21
5
|
|
22
6
|
SECRET_KEY_INSECURE_PREFIX = "plain-insecure-"
|
23
7
|
SECRET_KEY_MIN_LENGTH = 50
|
@@ -32,54 +16,18 @@ SECRET_KEY_WARNING_MSG = (
|
|
32
16
|
f"vulnerable to attack."
|
33
17
|
)
|
34
18
|
|
19
|
+
# TODO
|
35
20
|
W001 = Warning(
|
36
|
-
"You do not have 'plain.middleware.
|
21
|
+
"You do not have 'plain.middleware.https.HttpsRedirectMiddleware' "
|
37
22
|
"in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
|
38
23
|
"SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, "
|
39
|
-
"SECURE_CROSS_ORIGIN_OPENER_POLICY, and
|
24
|
+
"SECURE_CROSS_ORIGIN_OPENER_POLICY, and HTTPS_REDIRECT_ENABLED settings will "
|
40
25
|
"have no effect.",
|
41
26
|
id="security.W001",
|
42
27
|
)
|
43
28
|
|
44
|
-
W002 = Warning(
|
45
|
-
"You do not have "
|
46
|
-
"'plain.middleware.clickjacking.XFrameOptionsMiddleware' in your "
|
47
|
-
"MIDDLEWARE, so your pages will not be served with an "
|
48
|
-
"'x-frame-options' header. Unless there is a good reason for your "
|
49
|
-
"site to be served in a frame, you should consider enabling this "
|
50
|
-
"header to help prevent clickjacking attacks.",
|
51
|
-
id="security.W002",
|
52
|
-
)
|
53
|
-
|
54
|
-
W004 = Warning(
|
55
|
-
"You have not set a value for the SECURE_HSTS_SECONDS setting. "
|
56
|
-
"If your entire site is served only over SSL, you may want to consider "
|
57
|
-
"setting a value and enabling HTTP Strict Transport Security. "
|
58
|
-
"Be sure to read the documentation first; enabling HSTS carelessly "
|
59
|
-
"can cause serious, irreversible problems.",
|
60
|
-
id="security.W004",
|
61
|
-
)
|
62
|
-
|
63
|
-
W005 = Warning(
|
64
|
-
"You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. "
|
65
|
-
"Without this, your site is potentially vulnerable to attack "
|
66
|
-
"via an insecure connection to a subdomain. Only set this to True if "
|
67
|
-
"you are certain that all subdomains of your domain should be served "
|
68
|
-
"exclusively via SSL.",
|
69
|
-
id="security.W005",
|
70
|
-
)
|
71
|
-
|
72
|
-
W006 = Warning(
|
73
|
-
"Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, "
|
74
|
-
"so your pages will not be served with an "
|
75
|
-
"'X-Content-Type-Options: nosniff' header. "
|
76
|
-
"You should consider enabling this header to prevent the "
|
77
|
-
"browser from identifying content types incorrectly.",
|
78
|
-
id="security.W006",
|
79
|
-
)
|
80
|
-
|
81
29
|
W008 = Warning(
|
82
|
-
"Your
|
30
|
+
"Your HTTPS_REDIRECT_ENABLED setting is not set to True. "
|
83
31
|
"Unless your site should be available over both SSL and non-SSL "
|
84
32
|
"connections, you may want to either set this setting True "
|
85
33
|
"or configure a load balancer or reverse-proxy server "
|
@@ -102,99 +50,9 @@ W020 = Warning(
|
|
102
50
|
id="security.W020",
|
103
51
|
)
|
104
52
|
|
105
|
-
W021 = Warning(
|
106
|
-
"You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, "
|
107
|
-
"your site cannot be submitted to the browser preload list.",
|
108
|
-
id="security.W021",
|
109
|
-
)
|
110
|
-
|
111
|
-
W022 = Warning(
|
112
|
-
"You have not set the SECURE_REFERRER_POLICY setting. Without this, your "
|
113
|
-
"site will not send a Referrer-Policy header. You should consider "
|
114
|
-
"enabling this header to protect user privacy.",
|
115
|
-
id="security.W022",
|
116
|
-
)
|
117
|
-
|
118
|
-
E023 = Error(
|
119
|
-
"You have set the SECURE_REFERRER_POLICY setting to an invalid value.",
|
120
|
-
hint="Valid values are: {}.".format(", ".join(sorted(REFERRER_POLICY_VALUES))),
|
121
|
-
id="security.E023",
|
122
|
-
)
|
123
|
-
|
124
|
-
E024 = Error(
|
125
|
-
"You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid "
|
126
|
-
"value.",
|
127
|
-
hint="Valid values are: {}.".format(
|
128
|
-
", ".join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
|
129
|
-
),
|
130
|
-
id="security.E024",
|
131
|
-
)
|
132
|
-
|
133
53
|
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
134
54
|
|
135
55
|
|
136
|
-
def _security_middleware():
|
137
|
-
return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
|
138
|
-
|
139
|
-
|
140
|
-
def _xframe_middleware():
|
141
|
-
return (
|
142
|
-
"plain.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE
|
143
|
-
)
|
144
|
-
|
145
|
-
|
146
|
-
@register(deploy=True)
|
147
|
-
def check_security_middleware(package_configs, **kwargs):
|
148
|
-
passed_check = _security_middleware()
|
149
|
-
return [] if passed_check else [W001]
|
150
|
-
|
151
|
-
|
152
|
-
@register(deploy=True)
|
153
|
-
def check_xframe_options_middleware(package_configs, **kwargs):
|
154
|
-
passed_check = _xframe_middleware()
|
155
|
-
return [] if passed_check else [W002]
|
156
|
-
|
157
|
-
|
158
|
-
@register(deploy=True)
|
159
|
-
def check_sts(package_configs, **kwargs):
|
160
|
-
passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS
|
161
|
-
return [] if passed_check else [W004]
|
162
|
-
|
163
|
-
|
164
|
-
@register(deploy=True)
|
165
|
-
def check_sts_include_subdomains(package_configs, **kwargs):
|
166
|
-
passed_check = (
|
167
|
-
not _security_middleware()
|
168
|
-
or not settings.SECURE_HSTS_SECONDS
|
169
|
-
or settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True
|
170
|
-
)
|
171
|
-
return [] if passed_check else [W005]
|
172
|
-
|
173
|
-
|
174
|
-
@register(deploy=True)
|
175
|
-
def check_sts_preload(package_configs, **kwargs):
|
176
|
-
passed_check = (
|
177
|
-
not _security_middleware()
|
178
|
-
or not settings.SECURE_HSTS_SECONDS
|
179
|
-
or settings.SECURE_HSTS_PRELOAD is True
|
180
|
-
)
|
181
|
-
return [] if passed_check else [W021]
|
182
|
-
|
183
|
-
|
184
|
-
@register(deploy=True)
|
185
|
-
def check_content_type_nosniff(package_configs, **kwargs):
|
186
|
-
passed_check = (
|
187
|
-
not _security_middleware() or settings.SECURE_CONTENT_TYPE_NOSNIFF is True
|
188
|
-
)
|
189
|
-
return [] if passed_check else [W006]
|
190
|
-
|
191
|
-
|
192
|
-
@register(deploy=True)
|
193
|
-
def check_ssl_redirect(package_configs, **kwargs):
|
194
|
-
passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
|
195
|
-
return [] if passed_check else [W008]
|
196
|
-
|
197
|
-
|
198
56
|
def _check_secret_key(secret_key):
|
199
57
|
return (
|
200
58
|
len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
|
@@ -239,30 +97,3 @@ def check_debug(package_configs, **kwargs):
|
|
239
97
|
@register(deploy=True)
|
240
98
|
def check_allowed_hosts(package_configs, **kwargs):
|
241
99
|
return [] if settings.ALLOWED_HOSTS else [W020]
|
242
|
-
|
243
|
-
|
244
|
-
@register(deploy=True)
|
245
|
-
def check_referrer_policy(package_configs, **kwargs):
|
246
|
-
if _security_middleware():
|
247
|
-
if settings.SECURE_REFERRER_POLICY is None:
|
248
|
-
return [W022]
|
249
|
-
# Support a comma-separated string or iterable of values to allow fallback.
|
250
|
-
if isinstance(settings.SECURE_REFERRER_POLICY, str):
|
251
|
-
values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(",")}
|
252
|
-
else:
|
253
|
-
values = set(settings.SECURE_REFERRER_POLICY)
|
254
|
-
if not values <= REFERRER_POLICY_VALUES:
|
255
|
-
return [E023]
|
256
|
-
return []
|
257
|
-
|
258
|
-
|
259
|
-
@register(deploy=True)
|
260
|
-
def check_cross_origin_opener_policy(package_configs, **kwargs):
|
261
|
-
if (
|
262
|
-
_security_middleware()
|
263
|
-
and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None
|
264
|
-
and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
|
265
|
-
not in CROSS_ORIGIN_OPENER_POLICY_VALUES
|
266
|
-
):
|
267
|
-
return [E024]
|
268
|
-
return []
|
plain/preflight/security/csrf.py
CHANGED
@@ -32,9 +32,5 @@ def check_csrf_middleware(package_configs, **kwargs):
|
|
32
32
|
|
33
33
|
@register(deploy=True)
|
34
34
|
def check_csrf_cookie_secure(package_configs, **kwargs):
|
35
|
-
passed_check = (
|
36
|
-
settings.CSRF_USE_SESSIONS
|
37
|
-
or not _csrf_middleware()
|
38
|
-
or settings.CSRF_COOKIE_SECURE is True
|
39
|
-
)
|
35
|
+
passed_check = not _csrf_middleware() or settings.CSRF_COOKIE_SECURE is True
|
40
36
|
return [] if passed_check else [W016]
|
plain/runtime/README.md
CHANGED
@@ -54,12 +54,8 @@ SECRET_KEY = environ["SECRET_KEY"]
|
|
54
54
|
DEBUG = environ.get("DEBUG", "false").lower() in ("true", "1", "yes")
|
55
55
|
|
56
56
|
MIDDLEWARE = [
|
57
|
-
"plain.middleware.security.SecurityMiddleware",
|
58
57
|
"plain.sessions.middleware.SessionMiddleware",
|
59
|
-
"plain.middleware.common.CommonMiddleware",
|
60
|
-
"plain.csrf.middleware.CsrfViewMiddleware",
|
61
58
|
"plain.auth.middleware.AuthenticationMiddleware",
|
62
|
-
"plain.middleware.clickjacking.XFrameOptionsMiddleware",
|
63
59
|
]
|
64
60
|
|
65
61
|
if DEBUG:
|