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, 2026Reviewed 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

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.