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, orcontextproperties - 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:
-
Gain log access: Compromise a developer's laptop with access to Supabase project logs, or exploit a misconfigured log aggregation tool (Datadog, CloudWatch, etc.)
-
Trigger errors: Send malformed requests to
/cancel-subscriptionendpoint 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"}' -
Extract credentials: Search logs for error entries containing:
-"Auth error:"followed by serialized error objects with tokens
- Stack traces from Paddle API failures showing theAuthorization: Bearer [key]header
- Error messages fromsupabase.auth.getUser()that include the original bearer token -
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:
- Type checking:
err instanceof Errorverifies the error is a proper Error object - Message extraction:
err.messageextracts only the human-readable message string, excluding:
- Error properties (liketoken,headers,context)
- Stack traces
- Nested error objects - 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 messagecancel-subscription/index.ts:105: Generic catch block sanitizes all errorsconsumeCredits.ts:38: Shared auth error handler applies the same patternreset-credits/index.ts: (Similar change in the truncated diff)
Why Each Change Was Necessary
Each file required updates because they all handle sensitive operations:
cancel-subscription/index.ts: Directly uses Paddle API key for payment cancellations—the highest-risk functionconsumeCredits.ts: Shared utility function called by multiple endpoints—needed consistent error handlingreset-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)incancel-subscription/index.ts:105exposed 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")atcancel-subscription/index.ts:6and authentication tokens from request headers - Sink:
console.error(err)at line 105 andconsole.error("Auth error:", authError)at line 41 in the same file, plus similar patterns inconsumeCredits.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
- CWE-532: Insertion of Sensitive Information into Log File
- CWE-209: Generation of Error Message Containing Sensitive Information
- OWASP Logging Cheat Sheet
- Deno Security Best Practices - Error Handling
- Semgrep Rule: Sensitive Data in Logs
- fix: the paddle payment api key is retrieved from en... in index.ts