security: Address critical security vulnerabilities and improve code quality

🔒 Security Fixes:
- Make webhook signature validation required for production
- Prevent information disclosure by returning 200 for all webhook responses
- Sanitize external error messages while preserving internal logging

🔧 Code Quality Improvements:
- Add URL validation to prevent localhost usage in production
- Create currency utilities for proper handling of non-centesimal currencies
- Replace unsafe 'any' types with type-safe ProviderData wrapper
- Add comprehensive input validation for amounts, currencies, and descriptions
- Set default Stripe API version for consistency

📦 New Features:
- Currency conversion utilities supporting JPY, KRW, and other special cases
- Type-safe provider data structure with metadata
- Enhanced validation functions for payment data

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-17 18:38:44 +02:00
parent 209b683a8a
commit bf9940924c
6 changed files with 197 additions and 21 deletions

View File

@@ -10,6 +10,7 @@ import {
updateInvoiceOnPaymentSuccess,
handleWebhookError
} from './utils'
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
const symbol = Symbol('mollie')
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
@@ -105,20 +106,48 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
singleton.set(payload, mollieClient)
},
initPayment: async (payload, payment) => {
// Validate required fields
if (!payment.amount) {
throw new Error('Amount is required')
}
if (!payment.currency) {
throw new Error('Currency is required')
}
// Validate amount
if (!isValidAmount(payment.amount)) {
throw new Error('Invalid amount: must be a positive integer within reasonable limits')
}
// Validate currency code
if (!isValidCurrencyCode(payment.currency)) {
throw new Error('Invalid currency: must be a 3-letter ISO code')
}
// Validate URLs in production
const isProduction = process.env.NODE_ENV === 'production'
const redirectUrl = mollieConfig.redirectUrl ||
(!isProduction ? 'https://localhost:3000/payment/success' : undefined)
const webhookUrl = mollieConfig.webhookUrl ||
`${process.env.PAYLOAD_PUBLIC_SERVER_URL || (!isProduction ? 'https://localhost:3000' : '')}/api/payload-billing/mollie/webhook`
if (isProduction) {
if (!redirectUrl || redirectUrl.includes('localhost')) {
throw new Error('Valid redirect URL is required for production')
}
if (!webhookUrl || webhookUrl.includes('localhost')) {
throw new Error('Valid webhook URL is required for production')
}
}
const molliePayment = await singleton.get(payload).payments.create({
amount: {
value: (payment.amount / 100).toFixed(2),
currency: payment.currency
value: formatAmountForProvider(payment.amount, payment.currency),
currency: payment.currency.toUpperCase()
},
description: payment.description || '',
redirectUrl: mollieConfig.redirectUrl || 'https://localhost:3000/payment/success',
webhookUrl: mollieConfig.webhookUrl || `${process.env.PAYLOAD_PUBLIC_SERVER_URL || 'https://localhost:3000'}/api/payload-billing/mollie/webhook`,
redirectUrl,
webhookUrl,
});
payment.providerId = molliePayment.id
payment.providerData = molliePayment.toPlainObject()