#903·midday

Security Audit: 7 Findings (2 Medium, 3 Low, 2 Info)

Author: pointunbalanceCreated Aug 18, 2026Updated Aug 18, 2026

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:

typescript
} 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:

  1. Send a POST request to /webhook/polar with valid JSON but headers that cause validateEvent() to throw a non-WebhookVerificationError exception
  2. The catch block will parse and process the event without verifying the signature

Recommended Fix:

typescript
} 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:

typescript
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=true

Recommended Fix:

typescript
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:

typescript
const pathParts = normalizedPath.split("/");
const pathTeamId = pathParts[0];
// Validates teamId UUID but not ".." in other segments

Mitigating Factors:

  • UUID validation on teamId prevents directory traversal via first segment
  • withFileAuth middleware validates token's teamId matches path teamId
  • Supabase Storage likely rejects .. sequences
  • Path is used only for Supabase storage, not local filesystem

Recommended Fix:

typescript
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:

typescript
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.

typescript
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:

typescript
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:95
  • apps/api/src/rest/routers/webhooks/stripe/index.ts:65
  • apps/api/src/utils/teller.ts:24

Recommended Fix: Add guard checks for critical secrets at startup:

typescript
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:

typescript
response_url: z.string(),
// ...
await fetch(response_url, { method: "POST", ... });

Mitigating Factors:

  • The request is HMAC-verified via verifySlackInteraction using timingSafeEqual
  • The HMAC signature guarantees the response_url was 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
WhatsApp 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.