From ed27501afcf5f729087601a44d6abf6d19e0ba22 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Fri, 19 Sep 2025 13:19:15 +0200 Subject: [PATCH] fix: add comprehensive input validation to test provider API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add proper request schema validation for ProcessPaymentRequest interface - Validate paymentId format and ensure it follows test_pay_ pattern - Validate scenarioId and method parameters with type safety - Replace unsafe 'as any' casting with proper validation functions - Add consistent JSON error responses with appropriate HTTP status codes - Improve error messages for better debugging and API usability Security improvements: - Prevent injection attacks through input validation - Ensure all API endpoints validate their inputs properly - Add format validation for payment IDs to prevent invalid requests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/providers/test.ts | 96 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/providers/test.ts b/src/providers/test.ts index 6f629fb..50ba01b 100644 --- a/src/providers/test.ts +++ b/src/providers/test.ts @@ -5,6 +5,58 @@ import type { Payload } from 'payload' import { handleWebhookError, logWebhookEvent } from './utils' import { isValidAmount, isValidCurrencyCode } from './currency' +// Request validation schemas +interface ProcessPaymentRequest { + paymentId: string + scenarioId: string + method: PaymentMethod +} + +// Validation functions +function validateProcessPaymentRequest(body: any): { isValid: boolean; data?: ProcessPaymentRequest; error?: string } { + if (!body || typeof body !== 'object') { + return { isValid: false, error: 'Request body must be a valid JSON object' } + } + + const { paymentId, scenarioId, method } = body + + if (!paymentId || typeof paymentId !== 'string') { + return { isValid: false, error: 'paymentId is required and must be a string' } + } + + if (!scenarioId || typeof scenarioId !== 'string') { + return { isValid: false, error: 'scenarioId is required and must be a string' } + } + + if (!method || typeof method !== 'string') { + return { isValid: false, error: 'method is required and must be a string' } + } + + // Validate method is a valid payment method + const validMethods: PaymentMethod[] = ['ideal', 'creditcard', 'paypal', 'applepay', 'banktransfer'] + if (!validMethods.includes(method as PaymentMethod)) { + return { isValid: false, error: `method must be one of: ${validMethods.join(', ')}` } + } + + return { + isValid: true, + data: { paymentId, scenarioId, method: method as PaymentMethod } + } +} + +function validatePaymentId(paymentId: string): { isValid: boolean; error?: string } { + if (!paymentId || typeof paymentId !== 'string') { + return { isValid: false, error: 'Payment ID is required and must be a string' } + } + + // Validate payment ID format (should match test payment ID pattern) + if (!paymentId.startsWith('test_pay_')) { + return { isValid: false, error: 'Invalid payment ID format' } + } + + return { isValid: true } +} + export type PaymentOutcome = 'paid' | 'failed' | 'cancelled' | 'expired' | 'pending' export type PaymentMethod = 'ideal' | 'creditcard' | 'paypal' | 'applepay' | 'banktransfer' @@ -145,13 +197,29 @@ export const testProvider = (testConfig: TestProviderConfig) => { // Extract payment ID from URL path const urlParts = req.url?.split('/') || [] const paymentId = urlParts[urlParts.length - 1] + if (!paymentId) { - return new Response('Payment ID required', { status: 400 }) + return new Response(JSON.stringify({ error: 'Payment ID required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + + // Validate payment ID format + const validation = validatePaymentId(paymentId) + if (!validation.isValid) { + return new Response(JSON.stringify({ error: validation.error }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) } const session = testPaymentSessions.get(paymentId) if (!session) { - return new Response('Payment session not found', { status: 404 }) + return new Response(JSON.stringify({ error: 'Payment session not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }) } // Generate test payment UI @@ -193,7 +261,17 @@ export const testProvider = (testConfig: TestProviderConfig) => { try { const payload = req.payload const body = await req.json?.() || {} - const { paymentId, scenarioId, method } = body as any + + // Validate request body + const validation = validateProcessPaymentRequest(body) + if (!validation.isValid) { + return new Response(JSON.stringify({ error: validation.error }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + + const { paymentId, scenarioId, method } = validation.data! const session = testPaymentSessions.get(paymentId) if (!session) { @@ -205,7 +283,7 @@ export const testProvider = (testConfig: TestProviderConfig) => { const scenario = scenarios.find(s => s.id === scenarioId) if (!scenario) { - return new Response(JSON.stringify({ error: 'Invalid scenario' }), { + return new Response(JSON.stringify({ error: 'Invalid scenario ID' }), { status: 400, headers: { 'Content-Type': 'application/json' } }) @@ -273,6 +351,7 @@ export const testProvider = (testConfig: TestProviderConfig) => { // Extract payment ID from URL path const urlParts = req.url?.split('/') || [] const paymentId = urlParts[urlParts.length - 1] + if (!paymentId) { return new Response(JSON.stringify({ error: 'Payment ID required' }), { status: 400, @@ -280,6 +359,15 @@ export const testProvider = (testConfig: TestProviderConfig) => { }) } + // Validate payment ID format + const validation = validatePaymentId(paymentId) + if (!validation.isValid) { + return new Response(JSON.stringify({ error: validation.error }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + const session = testPaymentSessions.get(paymentId) if (!session) { return new Response(JSON.stringify({ error: 'Payment session not found' }), {