Security Audit: 7 Findings (2 Medium, 3 Low, 2 Info)
Security Audit Report: midday-ai/midday
Date: August 18, 2026 Target: https://github.com/midday-ai/midday Scope: Full codebase security audit (API, Auth, Webhooks, File Handling, Authorization) Methodology: Static code analysis using AI security agents
Executive Summary
The midday-ai/midday codebase demonstrates strong security practices overall. The architecture uses layered defenses including Supabase JWT authentication, team-scoped data isolation, HMAC webhook verification, and comprehensive middleware chains. No critical or high-severity vulnerabilities were found.
Total Findings: 7 (0 Critical, 0 High, 2 Medium, 3 Low, 2 Info)
Findings
FINDING 1 — Polar Webhook Signature Verification Bypass (MEDIUM)
Severity: Medium
File: apps/api/src/rest/routers/webhooks/polar/index.ts:80-85
Description:
When the Polar webhook signature verification (validateEvent()) throws an exception that is NOT a WebhookVerificationError, the code catches it and falls back to parsing the raw body WITHOUT signature verification:
} catch (err) {
if (err instanceof WebhookVerificationError) {
// Rejected properly
}
// Falls through to raw JSON parsing — NO signature check
event = JSON.parse(rawBody);
}Impact: An attacker could craft a request that triggers a non-verification error from validateEvent() (e.g., malformed body, network failure during key fetch) while still being valid JSON. This would bypass signature verification entirely.
Proof of Concept:
- Send a POST request to
/webhook/polarwith valid JSON but headers that causevalidateEvent()to throw a non-WebhookVerificationErrorexception - The catch block will parse and process the event without verifying the signature
Recommended Fix:
} catch (err) {
if (err instanceof WebhookVerificationError) {
return c.json({ error: "Invalid signature" }, 401);
}
// Remove the fallback — require signature verification
return c.json({ error: "Webhook verification failed" }, 401);
}FINDING 2 — Content-Disposition Header Injection (MEDIUM)
Severity: Medium
File: apps/api/src/rest/routers/files/download.ts:115, 304
Description:
The filename parameter from user input is used directly in the Content-Disposition header without sanitization:
headers["Content-Disposition"] = `attachment; filename="${filename}"`;Impact: An attacker with a valid file key could craft download requests with malicious filenames to confuse users about what file they're downloading. While modern HTTP frameworks prevent CRLF injection, the filename itself can be manipulated.
Proof of Concept:
GET /files/download/file?fk=<valid_token>&id=<file_id>&filename="report.pdf";+malicious=trueRecommended Fix:
function sanitizeFilename(name: string): string {
return name.replace(/[^a-zA-Z0-9._-]/g, "").slice(0, 255);
}
// Then use: sanitizeFilename(filename)FINDING 3 — Incomplete Path Traversal Sanitization (LOW)
Severity: Low
File: apps/api/src/rest/routers/files/utils.ts:10-37
Description:
The normalizeAndValidatePath function validates the first path segment (teamId) as a UUID but does NOT explicitly reject .. sequences in subsequent path segments:
const pathParts = normalizedPath.split("/");
const pathTeamId = pathParts[0];
// Validates teamId UUID but not ".." in other segmentsMitigating Factors:
- UUID validation on teamId prevents directory traversal via first segment
withFileAuthmiddleware validates token's teamId matches path teamId- Supabase Storage likely rejects
..sequences - Path is used only for Supabase storage, not local filesystem
Recommended Fix:
if (pathParts.some(part => part === ".." || part === ".")) {
throw new HTTPException(400, { message: "Invalid path: contains traversal sequences" });
}FINDING 4 — Worker Admin Dashboard Optional Auth (LOW)
Severity: Low
File: apps/worker/src/index.ts
Description:
The worker's admin dashboard (BullMQ UI) is protected by optional Basic Auth (BOARD_USERNAME/BOARD_PASSWORD). If these environment variables are not set, the workbench is unprotected.
Impact: In development environments or misconfigured deployments, the BullMQ dashboard could be exposed without authentication, allowing job queue manipulation.
Mitigating Factors:
- Production deployments should have these env vars set
- The dashboard is typically not exposed to the public internet
Recommended Fix:
if (!process.env.BOARD_USERNAME || !process.env.BOARD_PASSWORD) {
console.warn("WARNING: Worker admin dashboard has no authentication!");
// Or: disable the dashboard entirely
}FINDING 5 — Inbox Webhook Auth Bypass When Env Vars Missing (LOW)
Severity: Low
File: apps/api/src/rest/routers/webhooks/inbox/index.ts:27-35
Description:
If both INBOX_WEBHOOK_USERNAME and INBOX_WEBHOOK_PASSWORD are unset, the Basic Auth middleware is skipped entirely. The IP whitelist becomes the only gate.
if (process.env.INBOX_WEBHOOK_USERNAME && process.env.INBOX_WEBHOOK_PASSWORD) {
// Basic Auth applied
} else {
// No auth — only IP whitelist
}Impact: In misconfigured deployments, the inbox webhook could rely solely on IP whitelist, which is less secure than multi-factor verification.
Recommended Fix:
if (!process.env.INBOX_WEBHOOK_USERNAME || !process.env.INBOX_WEBHOOK_PASSWORD) {
console.warn("WARNING: Inbox webhook has no Basic Auth configured!");
}FINDING 6 — Env Var Non-Null Assertions (INFO)
Severity: Info File: Multiple files (~50+)
Description:
Many files use process.env.SOME_SECRET! without null guards. If the env var is unset, this causes opaque runtime crashes rather than clear configuration errors.
Impact: Reliability concern — not a confidentiality issue. Missing env vars cause undefined behavior rather than secure failure.
Example Locations:
packages/encryption/src/index.ts:95apps/api/src/rest/routers/webhooks/stripe/index.ts:65apps/api/src/utils/teller.ts:24
Recommended Fix: Add guard checks for critical secrets at startup:
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
if (!STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is required");
}FINDING 7 — Slack Interaction response_url Not Explicitly Validated (INFO)
Severity: Info
File: apps/api/src/rest/routers/apps/slack/interactions.ts:111, 320
Description:
The response_url from Slack interactions is fetched without explicit URL validation:
response_url: z.string(),
// ...
await fetch(response_url, { method: "POST", ... });Mitigating Factors:
- The request is HMAC-verified via
verifySlackInteractionusingtimingSafeEqual - The HMAC signature guarantees the
response_urlwas issued by Slack - An attacker cannot forge a valid Slack signature
Verdict: Safe due to cryptographic verification. Consider adding URL validation as defense-in-depth.
Positive Security Observations
No IDOR Vulnerabilities Found
All 38+ tRPC routers and REST endpoints derive teamId from the authenticated session, not from user-supplied parameters. DB queries consistently filter with eq(table.teamId, sessionTeamId).
No SQL Injection Found
All database access uses Drizzle ORM parameterized queries or Supabase client methods. No string concatenation in SQL.
No Hardcoded Secrets Found
All secrets are loaded via process.env.*. Test/mock credentials in test files are acceptable.
Webhook Verification is Comprehensive
| Webhook | Verification Method | Status |
|---|---|---|
| Stripe | HMAC via Stripe SDK | ✅ Secure |
| Polar | Standard Webhooks signature | ⚠️ Fallback issue |
| Plaid | JWT (ES256) with JWKS | ✅ Secure |
| Teller | HMAC-SHA256 | ✅ Secure |
| Inbox | Basic Auth + IP whitelist | ✅ Secure |
| Telegram | Bot framework | ✅ Secure |
| Bot framework | ✅ Secure | |
| Sendblue | Bot framework | ✅ Secure |
| Slack | HMAC-SHA256 + timingSafeEqual | ✅ Secure |
| Supabase DB | HMAC-SHA256 + timingSafeEqual | ✅ Secure |
OAuth Implementation is Solid
- PKCE enforced for public OAuth clients
- State parameters validated via encryption (AES-256-GCM) or provider-specific mechanisms
- Redirect URLs sanitized via
sanitizeRedirectPath() - No open redirects detected
File Security is Well-Implemented
- File access uses JWT-based file keys
- TeamId validation on file paths
- File type validation for uploads
- Supabase Storage with team-scoped paths
Recommendations Summary
| Priority | Finding | Action |
|---|---|---|
| P2 | Polar webhook fallback | Remove fallback path or add explicit signature re-validation |
| P2 | Content-Disposition injection | Sanitize filename in files/download.ts |
| P3 | Path traversal incomplete | Add .. rejection to normalizeAndValidatePath |
| P3 | Worker admin auth | Add startup warning if auth env vars missing |
| P4 | Inbox webhook auth | Add startup warning if Basic Auth env vars missing |
| P4 | Env var guards | Add null checks for critical secrets at startup |
| P4 | Slack response_url | Add URL validation as defense-in-depth |
Conclusion
The midday-ai/midday codebase has a mature security posture. The architecture uses multiple layers of defense including JWT authentication, team-scoped data isolation, HMAC webhook verification, comprehensive middleware chains, and proper input validation. The findings are mostly defense-in-depth improvements rather than critical vulnerabilities.
The most actionable finding is the Polar webhook fallback (Finding 1) which could allow signature bypass under specific error conditions.
Source: midday-ai/midday