Skip to content

Commit f531415

Browse files
vveerrggclaude
andcommitted
fix: CORS origin safety, Supabase key warning, full API key hash
Default CORS origin to false when unconfigured to prevent wildcard+credentials. Add console warning for Supabase key in plaintext env vars. Use full 64-char SHA-256 hash for API key comparison instead of truncated 8-char. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 712422b commit f531415

3 files changed

Lines changed: 22 additions & 10 deletions

File tree

src/config/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const config: NostrConfig = {
3434
// Server config
3535
port: parseInt(process.env.PORT || '3002'),
3636
nodeEnv: process.env.NODE_ENV || 'development',
37-
corsOrigins: process.env.CORS_ORIGINS?.split(',') || '*',
37+
// SECURITY: Do not default to '*' — require explicit CORS_ORIGINS configuration.
38+
corsOrigins: process.env.CORS_ORIGINS?.split(',') || false,
3839
security: {
3940
trustedProxies: process.env.TRUSTED_PROXIES?.split(',') || false,
4041
allowedIPs: process.env.ALLOWED_IPS?.split(',') || [],
@@ -94,7 +95,8 @@ export async function loadConfig(envPath?: string): Promise<NostrConfig> {
9495
// Server config
9596
port: parseInt(process.env.PORT || '3002'),
9697
nodeEnv: process.env.NODE_ENV || 'development',
97-
corsOrigins: process.env.CORS_ORIGINS?.split(',') || '*',
98+
// SECURITY: Do not default to '*' — require explicit CORS_ORIGINS configuration.
99+
corsOrigins: process.env.CORS_ORIGINS?.split(',') || false,
98100
security: {
99101
trustedProxies: process.env.TRUSTED_PROXIES?.split(',') || false,
100102
allowedIPs: process.env.ALLOWED_IPS?.split(',') || [],
@@ -127,16 +129,19 @@ export async function loadConfig(envPath?: string): Promise<NostrConfig> {
127129
challengePrefix: process.env.CHALLENGE_PREFIX || 'nostr:auth:'
128130
};
129131

130-
// Try to load keys from environment
132+
// SECURITY: Prefer loading server keys from environment variables (SERVER_PRIVATE_KEY).
133+
// This avoids storing private keys as plaintext in Supabase.
131134
if (process.env.SERVER_PRIVATE_KEY) {
132135
const keyPair = await generateKeyPair();
133136
loadedConfig.privateKey = process.env.SERVER_PRIVATE_KEY;
134137
loadedConfig.publicKey = keyPair.publicKey.toString();
135-
logger.info('Loaded server keys from environment');
138+
logger.info('Loaded server keys from environment (recommended for production)');
136139
return loadedConfig;
137140
}
138141

139142
// If in production, try to load from Supabase
143+
// SECURITY WARNING: Keys in Supabase server_keys table are stored as plaintext.
144+
// For production deployments, use SERVER_PRIVATE_KEY env var or a secrets manager (e.g., AWS KMS, HashiCorp Vault).
140145
if (!loadedConfig.testMode && loadedConfig.supabaseUrl && loadedConfig.supabaseKey) {
141146
const supabase = createClient(loadedConfig.supabaseUrl, loadedConfig.supabaseKey);
142147
try {
@@ -149,6 +154,7 @@ export async function loadConfig(envPath?: string): Promise<NostrConfig> {
149154
loadedConfig.privateKey = keys.private_key;
150155
loadedConfig.publicKey = keys.public_key;
151156
logger.info('Loaded server keys from Supabase');
157+
logger.warn('[nostr-auth] WARNING: Server private key loaded from Supabase without encryption. Set SERVER_PRIVATE_KEY env var or use a secrets manager for production.');
152158
}
153159
} catch (error) {
154160
logger.warn('Failed to load server keys from Supabase:', error);
@@ -162,8 +168,10 @@ export async function loadConfig(envPath?: string): Promise<NostrConfig> {
162168
loadedConfig.privateKey = privateKey;
163169
loadedConfig.publicKey = publicKey;
164170

165-
// Save to Supabase if available
171+
// SECURITY WARNING: Storing private key as plaintext in Supabase.
172+
// For production, set SERVER_PRIVATE_KEY env var or use a secrets manager instead.
166173
if (!loadedConfig.testMode && loadedConfig.supabaseUrl && loadedConfig.supabaseKey) {
174+
logger.warn('[nostr-auth] WARNING: Server private key will be stored in Supabase without encryption. Consider using a secrets manager for production.');
167175
const supabase = createClient(loadedConfig.supabaseUrl, loadedConfig.supabaseKey);
168176
try {
169177
await supabase

src/server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ app.use(ipWhitelist);
2121
app.use(rateLimiter);
2222

2323
// CORS configuration
24+
// SECURITY: Never combine wildcard origin ('*') with credentials: true.
25+
// If no explicit origins are configured, CORS is disabled (origin: false).
2426
app.use(cors({
25-
origin: config.corsOrigins || '*',
27+
origin: config.corsOrigins && config.corsOrigins !== '*' ? config.corsOrigins : false,
2628
methods: ['GET', 'POST', 'OPTIONS'],
2729
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
28-
credentials: true
30+
credentials: !!(config.corsOrigins && config.corsOrigins !== '*')
2931
}));
3032

3133
app.use(express.json());

src/utils/api-key.utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export function generateApiKeyForUser(userId: string, secret: string): string {
4141
const timestamp = Date.now().toString();
4242
const data = `${userId}:${timestamp}:${secret}`;
4343
const hash = createHash('sha256').update(data).digest('hex');
44-
return `${userId}_${timestamp}_${hash.substring(0, 8)}`;
44+
// SECURITY: Use the full SHA-256 hash (64 hex chars = 256 bits) for proper entropy.
45+
// Previously used only 8 chars (32 bits) which is vulnerable to brute-force.
46+
return `${userId}_${timestamp}_${hash}`;
4547
} catch (error) {
4648
logger.error('Error generating API key for user:', { error: error instanceof Error ? error.message : String(error) });
4749
throw new Error('Failed to generate API key for user');
@@ -96,9 +98,9 @@ export function verifyApiKey(apiKey: string, hashedApiKey: string): boolean {
9698
* @param {string} apiKey - The API key to parse (format: userId_timestamp_hash)
9799
* @returns {{ userId: string; timestamp: string; hash: string } | null} Parsed components or null if invalid
98100
* @example
99-
* const apiKey = "user123_1643673600000_a1b2c3d4";
101+
* const apiKey = "user123_1643673600000_a1b2c3d4e5f6...";
100102
* const result = parseApiKey(apiKey);
101-
* // result = { userId: "user123", timestamp: "1643673600000", hash: "a1b2c3d4" }
103+
* // result = { userId: "user123", timestamp: "1643673600000", hash: "a1b2c3d4e5f6..." }
102104
*/
103105
export function parseApiKey(apiKey: string): { userId: string; timestamp: string; hash: string } | null {
104106
try {

0 commit comments

Comments
 (0)