mirror of
https://github.com/usestrix/strix.git
synced 2026-09-22 00:31:25 +00:00
Fix false positives: JSON-stored XSS requires HTML render confirmation, NoSQL 500 errors are type mismatch not injection
- xss.md: Add critical JSON context rule — payload stored in JSON API response is NOT XSS until confirmed in HTML page (text/html) via Playwright. Add to Real Impact Gate as question #0 and to False Positive Rejection Rules as most common FP category. - nosql_injection.md: Add Real Impact Gate at top explicitly rejecting HTTP 500 (type mismatch) and WAF 403-vs-500 differential as proof of injection. Define mandatory exploitation requirements: auth bypass, boolean differential, $regex extraction, or $where timing. Add severity table: 500-only = Informational, bypass demonstrated = Critical. - system_prompt.jinja: Add JSON-context XSS rule to global XSS testing section. Add NoSQL injection evidence gate forbidding 500-based reports. Both now globally enforced across all agents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
948a11e75b
commit
b279d5cc4b
3 changed files with 113 additions and 4 deletions
|
|
@ -421,6 +421,15 @@ INJECTION TESTING (ALL PARAMETERS — MANDATORY):
|
|||
XSS TESTING — EXECUTION REQUIRED (ALL CONTEXTS):
|
||||
CRITICAL RULE: XSS is ONLY confirmed when the payload EXECUTES in a headless browser.
|
||||
A payload reflected in HTML source WITHOUT browser execution = NOT CONFIRMED = DO NOT REPORT.
|
||||
|
||||
CRITICAL RULE — JSON API RESPONSES ARE NEVER XSS:
|
||||
FORBIDDEN: Reporting XSS based solely on a JSON API response containing the payload.
|
||||
JSON responses (Content-Type: application/json) are NEVER rendered as HTML by browsers.
|
||||
A payload stored in {"first_name": "<img onerror=alert(1)>"} inside a JSON response = NOT XSS.
|
||||
MANDATORY: For stored XSS, you MUST navigate to the HTML page (Content-Type: text/html) that RENDERS the stored value and confirm execution there with Playwright.
|
||||
The test is NOT complete until you have: (1) found the HTML UI page that displays the stored field, (2) navigated to it with a headless browser, (3) confirmed alert/dialog/console execution.
|
||||
"The JSON response body contains the payload verbatim" = STORAGE CONFIRMED, XSS NOT YET CONFIRMED.
|
||||
|
||||
- Reflected XSS: test all URL parameters that appear in the response
|
||||
- Stored XSS: test all inputs stored and displayed to other users (messages, comments, profile fields, usernames, filenames)
|
||||
- DOM XSS: analyze JavaScript source for sink usage (innerHTML, document.write, eval, setTimeout, etc.), test URL hash/fragment
|
||||
|
|
@ -429,6 +438,19 @@ A payload reflected in HTML source WITHOUT browser execution = NOT CONFIRMED = D
|
|||
- XSS via file uploads: SVG with embedded script, HTML file upload, EXIF metadata in images
|
||||
- MANDATORY CONFIRMATION: Use headless browser to navigate to the reflected/stored XSS URL and confirm alert/console.log execution
|
||||
|
||||
NOSQL INJECTION — EVIDENCE GATE:
|
||||
CRITICAL RULE: HTTP 500 from sending an operator object where a string is expected is NOT confirmed injection.
|
||||
Sending {"field": {"$ne": null}} to a field that expects a string causes a framework/ODM TYPE MISMATCH error.
|
||||
The server throws 500 BEFORE the operator ever reaches MongoDB. This is input validation failure, NOT injection.
|
||||
FORBIDDEN: Reporting NoSQL injection based only on HTTP 500 errors from operator-as-object payloads.
|
||||
FORBIDDEN: Reporting "WAF bypass confirmed (403 string vs 500 object)" as Critical/High NoSQL injection.
|
||||
MANDATORY: To confirm NoSQL injection, you MUST demonstrate ONE of:
|
||||
(1) Authentication bypass — operator payload → login succeeds, you receive a valid session
|
||||
(2) Boolean differential — {"$gt": ""} returns data, {"$gt": "zzzzz"} returns empty — different results based on predicate truth
|
||||
(3) Blind extraction — $regex character enumeration extracts an actual token/password character by character
|
||||
(4) Timing via $where — sleep(3000) payload causes measurable response delay vs control request
|
||||
HTTP 500 alone → at most Informational ("application may not validate input types"). NEVER High or Critical.
|
||||
|
||||
AUTHENTICATION & SESSION ATTACKS:
|
||||
- JWT attacks: none algorithm, RS256→HS256 confusion with public key as HMAC secret, weak JWT secrets (wordlist brute force with jwt_tool), claim manipulation (sub, role, iat, exp)
|
||||
- Session fixation, session hijacking, concurrent session abuse
|
||||
|
|
|
|||
|
|
@ -7,6 +7,66 @@ description: NoSQL injection testing covering MongoDB operator injection, authen
|
|||
|
||||
NoSQL injection exploits the mismatch between how applications pass user input to database queries and how the database engine interprets that input. Unlike SQL injection, NoSQL injection frequently involves operator injection (e.g., MongoDB's `$gt`, `$regex`, `$where`) or structure injection (embedding JSON sub-documents). The attack surface is broad: MongoDB is the dominant target, but Redis, Elasticsearch, DynamoDB, Cassandra, and CouchDB each have distinct injection surfaces.
|
||||
|
||||
---
|
||||
|
||||
## Real Impact Gate — MANDATORY Before Reporting
|
||||
|
||||
Before reporting ANY NoSQL injection finding, you MUST answer ALL of the following:
|
||||
|
||||
### FORBIDDEN — These are NOT proof of NoSQL injection:
|
||||
|
||||
**HTTP 500 from type mismatch is NOT injection:**
|
||||
- Sending `{"field": {"$ne": null}}` where the application expects a string produces a 500 because the framework/ODM receives an object instead of a string and throws a type validation error
|
||||
- This is equivalent to sending `{"age": "not_a_number"}` — the server errors, but that is input validation failure, not operator injection
|
||||
- `500 Internal Server Error` + `{"message": "There was an unknown application error"}` = type mismatch, NOT confirmed injection
|
||||
- Mongoose `strict` mode, Joi validation, or any typed schema will produce 500 on object-in-string-field WITHOUT ever touching MongoDB
|
||||
|
||||
**WAF differential (403 string vs 500 object) is NOT injection:**
|
||||
- String `{"$ne": null}` gets WAF-blocked (403) = WAF works correctly on strings
|
||||
- Object `{"$ne": null}` gets 500 = application rejects malformed input
|
||||
- This differential proves WAF bypass for detection, NOT that MongoDB processed the operator
|
||||
- Do NOT report this as "NoSQL injection confirmed via WAF bypass"
|
||||
|
||||
### REQUIRED — One of these MUST be demonstrated:
|
||||
|
||||
1. **Authentication bypass**: Send operator payload to login endpoint → you receive a valid session token / are logged in as a real user → **HIGH/CRITICAL**
|
||||
|
||||
2. **Boolean differential on sensitive endpoint**:
|
||||
```
|
||||
{"field": {"$gt": ""}} → HTTP 200 with data
|
||||
{"field": {"$gt": "zzzzz"}} → HTTP 200 with NO data (empty array/null)
|
||||
```
|
||||
Different data responses based on predicate truth = confirmed operator injection → **HIGH**
|
||||
|
||||
3. **Blind data extraction via $regex**:
|
||||
```
|
||||
{"email": "admin@target.com", "token": {"$regex": "^a"}} → 200 (token starts with 'a')
|
||||
{"email": "admin@target.com", "token": {"$regex": "^b"}} → 401 (token does not start with 'b')
|
||||
```
|
||||
Extract actual secret character by character → **CRITICAL**
|
||||
|
||||
4. **Timing via $where** (MongoDB < 4.4):
|
||||
```
|
||||
{"$where": "function(){sleep(3000); return true}"} → response takes 3+ seconds
|
||||
{"$where": "function(){return true}"} → response is instant
|
||||
```
|
||||
Confirmed sleep differential = server-side JS execution → **HIGH**
|
||||
|
||||
### Severity Classification
|
||||
|
||||
| Evidence | Severity |
|
||||
|---|---|
|
||||
| Authentication bypass demonstrated | Critical |
|
||||
| Actual secret/token extracted via $regex | Critical |
|
||||
| Boolean differential + sensitive data widened | High |
|
||||
| $where timing confirmed | High |
|
||||
| Error-based indication only (500 errors, no exploit) | Low/Info |
|
||||
| WAF bypass differential (403 string vs 500 object) alone | Informational |
|
||||
|
||||
**RULE**: HTTP 500 from operator-as-object is at most **Informational** ("application may not properly reject non-string input") unless you can escalate it to a boolean differential or actual bypass. Do NOT report it as High or Critical.
|
||||
|
||||
---
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**MongoDB**
|
||||
|
|
@ -195,13 +255,20 @@ SELECT * FROM Users WHERE username = 'x' OR '1'='1
|
|||
4. Provide before/after: normal request returns 401, injected request returns 200
|
||||
5. For `$where`: show timing differential with/without `sleep()`
|
||||
|
||||
## False Positives
|
||||
## False Positives — DISCARD These Immediately
|
||||
|
||||
**DO NOT REPORT as NoSQL injection:**
|
||||
|
||||
- **HTTP 500 on operator-as-object with no further exploitation** — this is a type mismatch/validation error, not confirmed injection. The framework sees `{$ne: null}` where it expects a string and throws an unhandled exception. The operator NEVER reaches MongoDB.
|
||||
- **WAF 403 (string version) vs App 500 (object version)** — proves WAF only inspects string-encoded content, does NOT prove injection reached the database
|
||||
- **Consistent 500 errors across all operator types** — if every single operator (`$ne`, `$gt`, `$regex`, `$where`, `$exists`, etc.) produces the same 500 response, this is uniform type validation rejection, not injection
|
||||
- Framework-level query builder that casts input to string before constructing the query (Mongoose `strict` mode on)
|
||||
- Input sanitization stripping operator keys before they reach the driver
|
||||
- Endpoints that accept JSON but cast the `password` field to string — operator object becomes `[object Object]`
|
||||
- Endpoints that accept JSON but cast the field to string — operator object becomes `[object Object]`
|
||||
- Response differences caused by validation errors, not actual operator execution
|
||||
|
||||
**Escalation path**: If you observe 500 errors, attempt to find a LOGIN or SEARCH endpoint where you can demonstrate a boolean differential. If no such endpoint exists and you cannot bypass auth or extract data, this is at most **Informational**.
|
||||
|
||||
## Impact
|
||||
|
||||
- Authentication bypass granting access to arbitrary or all accounts
|
||||
|
|
|
|||
|
|
@ -9,12 +9,21 @@ XSS allows attackers to execute malicious JavaScript in victims' browsers, leadi
|
|||
|
||||
**CRITICAL RULE: A finding is XSS ONLY if it executes in a browser. HTML reflection without execution is NOT XSS. Always confirm browser execution before reporting.**
|
||||
|
||||
**CRITICAL RULE: JSON API RESPONSES ARE NEVER XSS.** If the endpoint returns `Content-Type: application/json`, the payload stored inside that JSON will NEVER execute as JavaScript — browsers parse JSON as data, not HTML. A payload stored in a JSON field and returned in a JSON response is NOT XSS until you prove it is rendered in an HTML context (text/html page) in a real browser.
|
||||
|
||||
---
|
||||
|
||||
## Real Impact Gate — Answer Before Reporting
|
||||
|
||||
Before reporting any XSS finding, explicitly answer ALL of these:
|
||||
|
||||
0. **Is the payload rendered in an HTML context (Content-Type: text/html)?**
|
||||
- MANDATORY FIRST CHECK: identify the UI page that renders the stored/reflected value
|
||||
- If you only have evidence of storage in a JSON API response → NOT confirmed XSS
|
||||
- You MUST navigate (with Playwright or browser) to the HTML page that displays the stored value and confirm execution there
|
||||
- "The JSON response body contains the payload verbatim" = STORAGE CONFIRMED, XSS NOT CONFIRMED
|
||||
- "The profile page at /users/123 rendered the payload and alert() fired" = XSS CONFIRMED
|
||||
|
||||
1. **Did the payload EXECUTE in a browser?** (Not just reflect in source — actually execute)
|
||||
- Required proof: alert/console.log capture from headless browser, or screenshot of execution, or screenshot of exfiltrated data
|
||||
|
||||
|
|
@ -460,11 +469,22 @@ An authenticated attacker can inject persistent JavaScript into their user profi
|
|||
|
||||
## False Positive Rejection Rules
|
||||
|
||||
Mark as FALSE POSITIVE and discard (do NOT report as vulnerability) if:
|
||||
Mark as FALSE POSITIVE and discard (do NOT report as vulnerability) if ANY of these apply:
|
||||
|
||||
**JSON CONTEXT — MOST COMMON FALSE POSITIVE:**
|
||||
- **The only evidence is a JSON API response containing the payload** → NOT XSS. JSON responses (Content-Type: application/json) are never rendered as HTML by browsers. The payload `<img onerror=alert(1)>` stored in `{"first_name": "<img onerror=alert(1)>"}` will never execute. You MUST find the HTML page that renders this value and confirm execution there.
|
||||
- The storage endpoint returns JSON and you have NOT navigated to the HTML UI page that renders the stored data → UNCONFIRMED, do not report
|
||||
- You assumed "navigate to profile page and it will execute" without actually doing it → NOT confirmed, do not report
|
||||
|
||||
**ENCODING / ESCAPING:**
|
||||
- Payload reflects in HTML but all special characters are HTML-encoded (`<`, `"`, etc.) → NOT XSS
|
||||
- Payload reflects in HTML but JavaScript cannot be triggered from that context → NOT exploitable XSS
|
||||
|
||||
**SCOPE / REACH:**
|
||||
- Self-XSS: payload only executes when the attacker submits it in their own browser, with no path to affect other users → Informational only
|
||||
- XSS in an admin-only panel where the admin themselves is the only viewer → self-XSS, Informational
|
||||
|
||||
**DEFENSES IN PLACE:**
|
||||
- CSP with strict nonces/hashes and no unsafe-inline or wildcard domains → XSS not exploitable without CSP bypass
|
||||
- Trusted Types enforced on all sinks → XSS not exploitable without Trusted Types bypass
|
||||
- Alert fires only in developer-mode console with no real execution path → NOT a valid XSS
|
||||
- XSS in an admin-only panel where the admin themselves is the only viewer → self-XSS, Informational
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue