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.

Prevention & Best Practices

1. Implement Structured Logging with Explicit Fields

Never log entire objects. Use structured logging libraries that require explicit field declaration:

// Bad
console.error("Error:", error);

// Good
import { logger } from './logger';
logger.error({
  message: error.message,
  code: error.code,
  userId: user.id,
  // Never include: error.stack, error.context, raw tokens
});

2. Create Error Sanitization Utilities

Build reusable functions for safe error logging:

function sanitizeError(err: unknown): string {
  if (err instanceof Error) {
    // Remove sensitive patterns from message
    return err.message.replace(/Bearer\s+[\w-]+/g, 'Bearer [REDACTED]')
                      .replace(/key=[\w-]+/g, 'key=[REDACTED]');
  }
  return 'Unknown error';
}

console.error("Operation failed:", sanitizeError(err));

3. Use Secret Scanning in CI/CD

Implement pre-commit hooks and CI checks:

# .github/workflows/security.yml
- name: Scan for secrets in logs
  uses: trufflesecurity/trufflehog@main
  with:
    path: ./logs
    fail-on-detection: true

4. Apply Log Filtering at Infrastructure Level

Configure log aggregation tools to redact sensitive patterns:

// Datadog log pipeline
{
  "type": "string-builder-processor",
  "name": "Redact API keys",
  "template": "{{message | regex_replace('[A-Za-z0-9]{32,}', '[REDACTED]')}}",
  "target": "message"
}

5. Follow OWASP Logging Guidelines

Implement OWASP Logging Cheat Sheet recommendations:

  • Never log: Passwords, API keys, tokens, session IDs, credit card numbers
  • Always sanitize: User input, error objects, HTTP headers
  • Use log levels: DEBUG for detailed info (disabled in production), ERROR for sanitized issues
  • Restrict access: Logs should be accessible only to authorized personnel with audit trails

6. Leverage TypeScript for Type-Safe Error Handling

Define custom error types that exclude sensitive data:

class SafeError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    // Explicitly no sensitive fields
  ) {
    super(message);
  }
}

// Safe to log
console.error(new SafeError("Payment failed", "PAYMENT_ERROR"));

Security Standards References

  • CWE-532: Insertion of Sensitive Information into Log File
  • CWE-209: Generation of Error Message Containing Sensitive Information
  • OWASP A09:2021: Security Logging and Monitoring Failures
  • PCI DSS 3.4: Render PAN unreadable (applies to all sensitive data in logs)

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.

References

Frequently Asked Questions

What is Sensitive Data Exposure via Error Logging?

A vulnerability where error handlers log complete error objects that may contain sensitive data like API keys, tokens, or credentials embedded in error properties, stack traces, or error contexts. This exposes secrets to anyone with log access.

How do you prevent Sensitive Data Exposure in TypeScript?

Never log full error objects directly. Extract only the error message using `err instanceof Error ? err.message : String(err)`. Implement structured logging with explicit field filtering. Use secret scanning tools on logs. Redact sensitive patterns before logging.

What CWE is Sensitive Data Exposure via Error Logging?

CWE-532 (Insertion of Sensitive Information into Log File). Related CWEs include CWE-209 (Generation of Error Message Containing Sensitive Information) and CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor).

Is sanitizing error messages enough to prevent Sensitive Data Exposure?

Sanitizing error messages is a critical first step, but complete protection requires multiple layers: secure secret management (environment variables only), log filtering/redaction, restricted log access, secret scanning in CI/CD, and proper error handling that never includes sensitive data in error construction.

Can static analysis detect Sensitive Data Exposure?

Yes, static analysis tools can detect patterns like `console.error(err)` or `console.log(apiKey)` that directly log variables or objects. Advanced tools like Orbis AppSec use dataflow analysis to trace sensitive data from sources (environment variables, auth headers) to logging sinks, catching indirect exposure paths.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.

high

How Command Injection happens in Node.js child_process calls and how to fix it

A high-severity command injection vulnerability was discovered in `tools/utils/lang/helpers.ts` where the `prettier()` function passed a user-controllable `fileName` argument directly into a shell command string via `exec()`. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates shell interpolation entirely, preventing attackers from injecting arbitrary shell commands through malicious filenames.

high

How Quadratic CPU Consumption in YAML Parsing happens in JavaScript and how to fix it

A high-severity vulnerability in js-yaml versions 3.x and 4.x allowed attackers to cause quadratic CPU consumption through specially crafted YAML documents using the `!!omap` type. This denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was fixed by upgrading from js-yaml 4.3.0 to 4.3.1, protecting applications from algorithmic complexity attacks during YAML parsing.

high

How Arbitrary HTTP Header Injection via Prototype Pollution happens in JavaScript and how to fix it

A high-severity vulnerability (CVE-2026-42035) in axios version 1.13.5 allowed attackers to inject arbitrary HTTP headers through prototype pollution. The fix upgrades axios to version 1.18.0 in the frontend's dependency tree, which includes proper prototype chain validation when constructing HTTP request headers. This prevents attackers from manipulating outgoing requests to perform SSRF, session hijacking, or cache poisoning attacks.

critical

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote versions prior to 1.8.4, where unescaped line terminators allowed attackers to inject arbitrary shell commands through crafted input strings. The fix pins shell-quote to version 1.9.0 via a `package.json` overrides directive in the FabricExample project, ensuring all transitive dependencies resolve to the patched version. Left unaddressed, this vulnerability could have allowed arbitrary code execution on any