-
Notifications
You must be signed in to change notification settings - Fork 33
feat(dataviewer): add OWASP security middleware stack #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e377c1e
security(data): add OWASP security middleware stack (#118)
katriendg bd79b13
fix: add words to cspell terms
katriendg 5411b4b
test(security): add tests for detection error handling and health che…
katriendg 0f728f2
refactor(data): sanitize user-controlled values in logs to prevent lo…
katriendg 970a900
refactor(dataviewer): update dependency overrides in detection securi…
katriendg 276f397
fix: move Limiter into shared object and add tests
katriendg 6c2ec5b
Merge branch 'main' into feat/118-security-fastapi
katriendg 529836c
Merge branch 'main' into feat/118-security-fastapi
katriendg ba54e4c
Merge branch 'main' into feat/118-security-fastapi
WilliamBerryiii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| """OWASP security headers and request body size enforcement middleware.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import ClassVar | ||
|
|
||
| from starlette.responses import JSONResponse as _StarletteJSONResponse | ||
|
|
||
|
|
||
| class SecurityHeadersMiddleware: | ||
| """Inject OWASP security headers on all HTTP responses. | ||
|
|
||
| CSP is only applied to non-API paths to avoid interfering with | ||
| frontend dev servers that proxy API responses and inherit headers. | ||
| """ | ||
|
|
||
| HEADERS: ClassVar[list[tuple[bytes, bytes]]] = [ | ||
| (b"x-content-type-options", b"nosniff"), | ||
| (b"x-frame-options", b"DENY"), | ||
| (b"referrer-policy", b"strict-origin-when-cross-origin"), | ||
| (b"permissions-policy", b"geolocation=(), microphone=(), camera=()"), | ||
| (b"cross-origin-opener-policy", b"same-origin"), | ||
| ] | ||
|
|
||
| CSP_HEADER: ClassVar[tuple[bytes, bytes]] = ( | ||
| b"content-security-policy", | ||
| b"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " | ||
| b"img-src 'self' data: blob:; connect-src 'self'; font-src 'self'; object-src 'none'", | ||
| ) | ||
|
|
||
| _SKIP_PATHS: ClassVar[set[str]] = {"/docs", "/redoc", "/openapi.json"} | ||
|
|
||
| def __init__(self, app): | ||
| self.app = app | ||
|
|
||
| async def __call__(self, scope, receive, send): | ||
| if scope["type"] != "http": | ||
| await self.app(scope, receive, send) | ||
| return | ||
|
|
||
| path = scope.get("path", "") | ||
| if path in self._SKIP_PATHS: | ||
| await self.app(scope, receive, send) | ||
| return | ||
|
|
||
| is_api = path.startswith("/api") or path == "/health" | ||
| extra_headers = self.HEADERS if is_api else [*self.HEADERS, self.CSP_HEADER] | ||
|
|
||
| async def send_with_headers(message): | ||
| if message["type"] == "http.response.start": | ||
| headers = list(message.get("headers", [])) | ||
| headers.extend(extra_headers) | ||
| message = {**message, "headers": headers} | ||
| await send(message) | ||
|
|
||
| await self.app(scope, receive, send_with_headers) | ||
|
|
||
|
|
||
| class ContentSizeLimitMiddleware: | ||
| """Reject requests exceeding the configured body size limit. | ||
|
|
||
| Checks Content-Length header upfront and tracks actual bytes | ||
| received to catch chunked transfer-encoding requests. | ||
| """ | ||
|
|
||
| def __init__(self, app, max_content_length: int = 10 * 1024 * 1024): | ||
| self.app = app | ||
| self.max_content_length = max_content_length | ||
|
|
||
| async def __call__(self, scope, receive, send): | ||
| if scope["type"] != "http": | ||
| await self.app(scope, receive, send) | ||
| return | ||
|
|
||
| headers = dict(scope.get("headers", [])) | ||
| content_length = headers.get(b"content-length") | ||
|
|
||
| if content_length is not None: | ||
| try: | ||
| if int(content_length) > self.max_content_length: | ||
| response = _StarletteJSONResponse( | ||
| {"detail": "Request body too large"}, | ||
| status_code=413, | ||
| ) | ||
| await response(scope, receive, send) | ||
| return | ||
| except ValueError: | ||
| pass # Malformed Content-Length header; fall through to streaming byte check | ||
|
|
||
| bytes_received = 0 | ||
| limit = self.max_content_length | ||
|
|
||
| async def receive_with_limit(): | ||
| nonlocal bytes_received | ||
| message = await receive() | ||
| if message.get("type") == "http.request": | ||
| body = message.get("body", b"") | ||
| bytes_received += len(body) | ||
| if bytes_received > limit: | ||
| raise _BodyTooLargeError | ||
| return message | ||
|
|
||
| try: | ||
| await self.app(scope, receive_with_limit, send) | ||
| except _BodyTooLargeError: | ||
| response = _StarletteJSONResponse( | ||
| {"detail": "Request body too large"}, | ||
| status_code=413, | ||
| ) | ||
| await response(scope, receive, send) | ||
|
|
||
|
|
||
| class _BodyTooLargeError(Exception): | ||
| """Internal signal for body size limit exceeded.""" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| """Shared rate limiter instance for the entire API.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from slowapi import Limiter | ||
| from slowapi.util import get_remote_address | ||
|
|
||
| limiter = Limiter(key_func=get_remote_address) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.