create-caspian-app 1.3.19 → 1.3.20
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.
- package/dist/main.py +52 -0
- package/dist/settings/check_templates.py +17 -2
- package/package.json +1 -1
package/dist/main.py
CHANGED
|
@@ -27,6 +27,7 @@ from fastapi.responses import (
|
|
|
27
27
|
RedirectResponse,
|
|
28
28
|
HTMLResponse,
|
|
29
29
|
JSONResponse,
|
|
30
|
+
PlainTextResponse,
|
|
30
31
|
)
|
|
31
32
|
from starlette.datastructures import MutableHeaders
|
|
32
33
|
from starlette.middleware import Middleware
|
|
@@ -472,6 +473,54 @@ class SecurityHeadersMiddleware:
|
|
|
472
473
|
await self.app(scope, receive, send_wrapper)
|
|
473
474
|
|
|
474
475
|
|
|
476
|
+
# Top-level directories under `public/` are asset namespaces, not page routes:
|
|
477
|
+
# `public/js/**` owns `/js/**` and nothing in `src/app/` can answer there. Built
|
|
478
|
+
# once, like SECURITY_HEADERS -- adding a new asset *directory* is a restart-level
|
|
479
|
+
# change, unlike adding a file to an existing one, which stays live.
|
|
480
|
+
PUBLIC_ASSET_NAMESPACES: frozenset[str] = (
|
|
481
|
+
frozenset(entry.name.casefold() for entry in Path("public").iterdir() if entry.is_dir())
|
|
482
|
+
if Path("public").is_dir()
|
|
483
|
+
else frozenset()
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class MissingPublicAssetMiddleware:
|
|
488
|
+
"""404 a missing file in a public asset namespace instead of falling through.
|
|
489
|
+
|
|
490
|
+
`PublicFilesMiddleware` deliberately falls through when no file matches, so
|
|
491
|
+
normal routing keeps working. With `is_all_routes_private=True` that means a
|
|
492
|
+
missing asset reaches `AuthMiddleware` and answers `303 -> /signin`, which is
|
|
493
|
+
the wrong answer twice over: a `<script src="/js/typo.js">` then receives the
|
|
494
|
+
sign-in *page* as `200 text/html` and fails with a parse error that names the
|
|
495
|
+
wrong file, and every bogus asset path returns a full HTML page to anonymous
|
|
496
|
+
traffic. A path whose first segment is a real `public/` directory can only be
|
|
497
|
+
an asset request, so a miss there is a genuine 404.
|
|
498
|
+
|
|
499
|
+
Runs inside the rate limiter -- a 404 flood is still a flood -- but outside
|
|
500
|
+
sessions, CSRF, and auth, so a missing asset costs no session decryption.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def __init__(self, app: ASGIApp):
|
|
504
|
+
self.app = app
|
|
505
|
+
|
|
506
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
507
|
+
if scope["type"] != "http" or scope.get("method", "GET").upper() not in {
|
|
508
|
+
"GET",
|
|
509
|
+
"HEAD",
|
|
510
|
+
}:
|
|
511
|
+
await self.app(scope, receive, send)
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
segment = str(scope.get("path", "")).lstrip("/").split("/", 1)[0].casefold()
|
|
515
|
+
if segment not in PUBLIC_ASSET_NAMESPACES:
|
|
516
|
+
await self.app(scope, receive, send)
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
# PublicFilesMiddleware sits outside this one, so reaching here means it
|
|
520
|
+
# already declined: the file does not exist or escapes the public root.
|
|
521
|
+
await PlainTextResponse("Not Found", status_code=404)(scope, receive, send)
|
|
522
|
+
|
|
523
|
+
|
|
475
524
|
class BodySizeLimitMiddleware:
|
|
476
525
|
"""Reject oversized HTTP request bodies before route or RPC parsing."""
|
|
477
526
|
|
|
@@ -1534,6 +1583,9 @@ app.add_middleware(
|
|
|
1534
1583
|
path="/",
|
|
1535
1584
|
)
|
|
1536
1585
|
app.add_middleware(BodySizeLimitMiddleware)
|
|
1586
|
+
# Sits between the limiter and the session/auth layers: a miss under a public
|
|
1587
|
+
# asset namespace is a 404, not a sign-in redirect, and costs no session work.
|
|
1588
|
+
app.add_middleware(MissingPublicAssetMiddleware)
|
|
1537
1589
|
# Outermost of the security layers: reject flooding before any session
|
|
1538
1590
|
# decryption, template rendering, or database work is spent on the request.
|
|
1539
1591
|
app.add_middleware(RateLimitMiddleware)
|
|
@@ -90,7 +90,17 @@ RULES: list[Rule] = [
|
|
|
90
90
|
Rule(
|
|
91
91
|
"unquoted-brace-attr",
|
|
92
92
|
# `class={...}` / `selected={...}` -- invalid HTML, silently blanks the page.
|
|
93
|
-
|
|
93
|
+
#
|
|
94
|
+
# An attribute only exists *inside an opening tag*, and the rule must say
|
|
95
|
+
# so. A bare `\s[\w:.\-]+=\{` also matches `DIR={path}` in a shell script
|
|
96
|
+
# and `ENV PORT={port}` in a Dockerfile -- and this repo embeds both in
|
|
97
|
+
# triple-quoted strings under `src/lib/aws/`, which the Python scan keeps
|
|
98
|
+
# because it cannot tell a heredoc from a template. Requiring the opening
|
|
99
|
+
# tag is not a loosening: a real violation is always inside one.
|
|
100
|
+
#
|
|
101
|
+
# `[^<>]*?` bounds the attribute run to a single tag; it matches newlines
|
|
102
|
+
# (negated classes do), so an attribute on its own line is still caught.
|
|
103
|
+
re.compile(r"<[a-zA-Z][\w:.\-]*(?:[^<>]*?)?\s[\w:.\-]+=\{"),
|
|
94
104
|
"Unquoted brace attribute. This is invalid HTML: the parser splits the "
|
|
95
105
|
"value on spaces, the component root never compiles, and the page "
|
|
96
106
|
'renders blank with no console error. Quote it: attr="{expr}".',
|
|
@@ -116,7 +126,12 @@ RULES: list[Rule] = [
|
|
|
116
126
|
),
|
|
117
127
|
Rule(
|
|
118
128
|
"jsx-fragment",
|
|
119
|
-
|
|
129
|
+
# `</>` is unambiguous, and a well-formed fragment always has one. A bare
|
|
130
|
+
# `<>` is not: it is SQL's not-equals operator, and this repo runs
|
|
131
|
+
# `WHERE pid <> pg_backend_pid()` from a triple-quoted string. So the
|
|
132
|
+
# open tag counts only when an element follows it, which is what a
|
|
133
|
+
# fragment looks like and what `<> value` in SQL never does.
|
|
134
|
+
re.compile(r"</>|<>\s*<"),
|
|
120
135
|
"JSX fragment. A template needs exactly one real root element.",
|
|
121
136
|
),
|
|
122
137
|
Rule(
|