feat(cors-proxy): add anti-spoofing security measures
Security improvements for the Cloudflare Worker CORS proxy: - Add rate limiting per IP (60 requests/minute) using Cloudflare KV - Add file size limit (10MB max) to prevent abuse - Add HMAC signature verification (optional, for deterrence) - Add timestamp validation to prevent replay attacks - Block private IP ranges (localhost, 10.x, 192.168.x, 172.16-31.x) Client-side changes: - Add signature generation in digital-sign-pdf.ts - Add security warning about client-side secrets Documentation: - Update README with production security features - Update docs/self-hosting/cloudflare.md with CORS proxy section - Document KV setup for rate limiting - Add clear warnings about client-side HMAC limitations Files changed: - cloudflare/cors-proxy-worker.js - cloudflare/wrangler.toml - src/js/logic/digital-sign-pdf.ts - README.md - docs/self-hosting/cloudflare.md
This commit is contained in:
@@ -124,11 +124,58 @@ export function parseCombinedPem(pemContent: string, password?: string): Certifi
|
||||
*/
|
||||
const CORS_PROXY_URL = import.meta.env.VITE_CORS_PROXY_URL || '';
|
||||
|
||||
/**
|
||||
* Shared secret for signing proxy requests (HMAC-SHA256).
|
||||
*
|
||||
* SECURITY NOTE FOR PRODUCTION:
|
||||
* Client-side secrets are NEVER truly hidden and they can be extracted from
|
||||
* bundled JavaScript.
|
||||
*
|
||||
* For production deployments with sensitive requirements, you should:
|
||||
* 1. Use your own backend server to proxy certificate requests
|
||||
* 2. Keep the HMAC secret on your server ONLY (never in frontend code)
|
||||
* 3. Have your frontend call your server, which then calls the CORS proxy
|
||||
*
|
||||
* This client-side HMAC provides limited protection (deters casual abuse)
|
||||
* but should NOT be considered secure against determined attackers. BentoPDF
|
||||
* accepts this tradeoff because of it's client side architecture.
|
||||
*
|
||||
* To enable (optional):
|
||||
* 1. Generate a secret: openssl rand -hex 32
|
||||
* 2. Set PROXY_SECRET on your Cloudflare Worker: npx wrangler secret put PROXY_SECRET
|
||||
* 3. Set VITE_CORS_PROXY_SECRET in your build environment (must match PROXY_SECRET)
|
||||
*/
|
||||
const CORS_PROXY_SECRET = import.meta.env.VITE_CORS_PROXY_SECRET || '';
|
||||
|
||||
async function generateProxySignature(url: string, timestamp: number): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(CORS_PROXY_SECRET),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const message = `${url}${timestamp}`;
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
key,
|
||||
encoder.encode(message)
|
||||
);
|
||||
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom fetch wrapper that routes external certificate requests through a CORS proxy.
|
||||
* The zgapdfsigner library tries to fetch issuer certificates from URLs embedded in the
|
||||
* certificate's AIA extension. When those servers don't have CORS enabled (like www.cert.fnmt.es),
|
||||
* the fetch fails. This wrapper routes such requests through our CORS proxy.
|
||||
*
|
||||
* If VITE_CORS_PROXY_SECRET is configured, requests include HMAC signatures for anti-spoofing.
|
||||
*/
|
||||
function createCorsAwareFetch(): {
|
||||
wrappedFetch: typeof fetch;
|
||||
@@ -150,8 +197,17 @@ function createCorsAwareFetch(): {
|
||||
) && !url.startsWith(window.location.origin);
|
||||
|
||||
if (isExternalCertificateUrl && CORS_PROXY_URL) {
|
||||
const proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
||||
console.log(`[CORS Proxy] Routing certificate request through proxy: ${url}`);
|
||||
let proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
||||
|
||||
if (CORS_PROXY_SECRET) {
|
||||
const timestamp = Date.now();
|
||||
const signature = await generateProxySignature(url, timestamp);
|
||||
proxyUrl += `&t=${timestamp}&sig=${signature}`;
|
||||
console.log(`[CORS Proxy] Routing signed certificate request through proxy: ${url}`);
|
||||
} else {
|
||||
console.log(`[CORS Proxy] Routing certificate request through proxy: ${url}`);
|
||||
}
|
||||
|
||||
return originalFetch(proxyUrl, init);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user