Back to Blog
critical SEVERITY8 min read

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

O
By Orbis AppSec
•Published August 14, 2026•Reviewed August 14, 2026

Answer Summary

This is a Sensitive Data Exposure vulnerability (CWE-532: Insertion of Sensitive Information into Log File) in TypeScript/Deno Supabase Edge Functions. Error handlers in `cancel-subscription/index.ts`, `consumeCredits.ts`, and `reset-credits/index.ts` logged complete error objects with `console.error()`, which could include Paddle API keys, auth tokens, and other credentials in stack traces or error properties. The fix replaces `console.error(err)` with `console.error(err instanceof Error ? err.message : "Internal error")` and similar patterns to log only safe error messages, preventing credential exposure in logs.

Vulnerability at a Glance

cweCWE-532 (Insertion of Sensitive Information into Log File)
fixExtract and log only error messages, not full objects
riskAPI keys and authentication tokens exposed in deployment logs
languageTypeScript/Deno
root causeLogging full error objects without sanitization
vulnerabilitySensitive Data Exposure via Error Logging

Introduction

In a Supabase Edge Functions deployment, we discovered a critical Sensitive Data Exposure vulnerability in supabase/functions/cancel-subscription/index.ts at line 105. The vulnerability affected three serverless functions handling payment subscription management and credit consumption. The problematic pattern appeared in error handlers that logged complete error objects: console.error(err) and console.error("Auth error:", authError). These logging statements could expose the Paddle payment API key retrieved from Deno.env.get("PADDLE_API_KEY"), authentication tokens from request headers, and other sensitive credentials in deployment logs accessible to developers, DevOps teams, or attackers who compromise log aggregation systems.

This matters because serverless functions often handle API keys for third-party payment processors, and a single exposed key in logs can lead to unauthorized payment operations, subscription manipulation, or financial fraud. The vulnerability had an exploitation complexity of just 2 steps: gain access to deployment logs (through compromised CI/CD, log aggregation tools, or insider access) and extract credentials from error output.

The Vulnerability Explained

The vulnerable code pattern appeared in three locations across the Supabase functions. Here's the specific vulnerable code from cancel-subscription/index.ts:

// Line 41: Authentication error logging
if (authError || !user) {
  console.error("Auth error:", authError);
  return new Response("Invalid token", { status: 401 });
}

// Line 105: Generic error handler
} catch (err) {
  console.error(err);
  return new Response(
    JSON.stringify({ error: "Internal Server Error" }),
    { status: 500, headers: corsHeaders }
  );
}

The problem lies in logging the complete error object without sanitization. When console.error(authError) executes, JavaScript's default serialization can include:

  • Error properties: Custom error objects from Supabase auth might include token, headers, or context properties
  • Stack traces: Full call stacks that may reveal environment variables or function parameters
  • Nested objects: Error causes or wrapped errors that contain the original request data

In the context of this function, the Paddle API key is retrieved at line 6:

const PADDLE_API_KEY = Deno.env.get("PADDLE_API_KEY");

If an error occurs during the Paddle API call (lines 70-85), the error object could contain the Authorization header with the API key. When this error bubbles up to the catch block and gets logged with console.error(err), the full error—including HTTP headers—gets written to deployment logs.

Attack Scenario

An attacker exploits this vulnerability through the following concrete steps:

  1. Gain log access: Compromise a developer's laptop with access to Supabase project logs, or exploit a misconfigured log aggregation tool (Datadog, CloudWatch, etc.)

  2. Trigger errors: Send malformed requests to /cancel-subscription endpoint to trigger authentication failures or API errors:
    bash curl -X POST https://[project].supabase.co/functions/v1/cancel-subscription \ -H "Authorization: Bearer invalid_token" \ -H "Content-Type: application/json" \ -d '{"subscription_id": "malformed"}'

  3. Extract credentials: Search logs for error entries containing:
    - "Auth error:" followed by serialized error objects with tokens
    - Stack traces from Paddle API failures showing the Authorization: Bearer [key] header
    - Error messages from supabase.auth.getUser() that include the original bearer token

  4. Exploit stolen keys: Use the extracted Paddle API key to:
    - Cancel arbitrary subscriptions
    - Modify payment plans
    - Refund transactions
    - Access customer payment information

The real-world impact is severe: Paddle API keys provide full access to the payment processor account, potentially affecting thousands of customers and causing direct financial loss.

The Fix

The fix implements error message sanitization across all three affected files. Here's the specific change made to cancel-subscription/index.ts:

Before (vulnerable code):

if (authError || !user) {
  console.error("Auth error:", authError);
  return new Response("Invalid token", { status: 401 });
}

// ... later ...

} catch (err) {
  console.error(err);
  return new Response(
    JSON.stringify({ error: "Internal Server Error" }),
    { status: 500, headers: corsHeaders }
  );
}

After (secure code):

if (authError || !user) {
  console.error("Auth error:", authError instanceof Error ? authError.message : String(authError));
  return new Response("Invalid token", { status: 401 });
}

// ... later ...

} catch (err) {
  console.error(err instanceof Error ? err.message : "Internal error");
  return new Response(
    JSON.stringify({ error: "Internal Server Error" }),
    { status: 500, headers: corsHeaders }
  );
}

How This Fix Works

The change introduces type-safe error message extraction:

  1. Type checking: err instanceof Error verifies the error is a proper Error object
  2. Message extraction: err.message extracts only the human-readable message string, excluding:
    - Error properties (like token, headers, context)
    - Stack traces
    - Nested error objects
  3. Fallback handling: String(authError) or "Internal error" handles edge cases where the error isn't an Error instance

This pattern was applied to three locations:

  • cancel-subscription/index.ts:41: Auth error logging now extracts only the message
  • cancel-subscription/index.ts:105: Generic catch block sanitizes all errors
  • consumeCredits.ts:38: Shared auth error handler applies the same pattern
  • reset-credits/index.ts: (Similar change in the truncated diff)

Why Each Change Was Necessary

Each file required updates because they all handle sensitive operations:

  1. cancel-subscription/index.ts: Directly uses Paddle API key for payment cancellations—the highest-risk function
  2. consumeCredits.ts: Shared utility function called by multiple endpoints—needed consistent error handling
  3. reset-credits/index.ts: Administrative function with elevated privileges—equally sensitive

The fix preserves debugging capability (developers still see error messages) while eliminating credential exposure. The error message "Invalid token" or "Internal error" provides enough context for troubleshooting without revealing the actual token value or API key.

Key Takeaways

  • Never log full error objects in Supabase Edge Functions: The pattern console.error(err) in cancel-subscription/index.ts:105 exposed Paddle API keys and auth tokens through complete error serialization
  • Extract only error messages: The fix err instanceof Error ? err.message : "Internal error" prevents credential leakage while maintaining debugging capability
  • Sanitize authentication errors consistently: All three files (cancel-subscription, consumeCredits, reset-credits) required the same pattern because they all handle sensitive auth flows
  • Serverless functions need extra logging scrutiny: Edge Functions run in shared environments where logs may be aggregated across multiple tenants, increasing exposure risk
  • 2-step exploit chains are critical: This vulnerability required only log access + error triggering, making it highly exploitable in real-world scenarios

How Orbis AppSec Detected This

  • Source: Paddle API key retrieved from Deno.env.get("PADDLE_API_KEY") at cancel-subscription/index.ts:6 and authentication tokens from request headers
  • Sink: console.error(err) at line 105 and console.error("Auth error:", authError) at line 41 in the same file, plus similar patterns in consumeCredits.ts:38
  • Missing control: No error sanitization or message extraction before logging; error objects logged directly without filtering sensitive properties
  • CWE: CWE-532 (Insertion of Sensitive Information into Log File)
  • Fix: Replaced direct error logging with type-safe message extraction: err instanceof Error ? err.message : "Internal error"

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability demonstrates how seemingly innocuous logging practices can create critical security exposures in serverless architectures. The cancel-subscription function's error handlers logged complete error objects, potentially exposing Paddle API keys worth thousands of dollars in unauthorized payment operations. By implementing type-safe error message extraction across all three affected files, the fix eliminates credential exposure while preserving debugging capability.

The key lesson: treat error objects as untrusted data containers. Just as you sanitize user input before database queries, sanitize error objects before logging. In TypeScript/Deno environments handling payment APIs, this practice is not optional—it's a critical security control that prevents credential theft through log access.

Apply the pattern err instanceof Error ? err.message : String(err) consistently in all error handlers, implement structured logging with explicit field allowlists, and use automated tools like Orbis AppSec to catch these vulnerabilities before they reach production.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li

high

js-yaml 4.3.1 Denial of Service: Malformed Input Hangs YAML Parser

A denial of service vulnerability in js-yaml versions 4.3.1 and earlier allows attackers to hang the YAML parser indefinitely by providing specially crafted malformed input. The fix, released in versions 4.3.2 and 3.15.2, patches the parsing logic to prevent unbounded processing. Upgrading is recommended for all applications parsing untrusted YAML data.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

critical

Updater.parseUpdate() CWE-494: Unsigned Metadata Download

The parseUpdate function in the Updater component extracted download URLs from remote server responses without cryptographic verification, enabling supply chain attacks via compromised or spoofed update servers. The fix adds strict URL validation requiring HTTPS and a trusted hostname before accepting any update metadata.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.

high

CVE-2026-67213: nanoid customAlphabet Infinite Loop Fix

nanoid, a widely-used ID generator pulled in transitively through postcss and vitepress, had an infinite-loop bug in its `customAlphabet` code path before version 5.1.6. This PR pins the entire dependency tree to nanoid 5.1.16 via a pnpm override so no transitive consumer can resolve back to the vulnerable 3.3.16 release.