ship-safe 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -19
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/cli/bin/ship-safe.js +8 -1
- package/cli/commands/init.js +2 -1
- package/cli/utils/patterns.js +345 -24
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/gitignore-template +258 -0
- package/configs/supabase/rls-templates.sql +242 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/package.json +12 -3
- package/snippets/README.md +89 -25
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
package/cli/utils/patterns.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* MAINTENANCE NOTES:
|
|
13
13
|
* - Patterns should have low false-positive rates
|
|
14
|
+
* - Only include patterns with SPECIFIC PREFIXES to avoid noise
|
|
14
15
|
* - Test new patterns against real codebases before adding
|
|
15
16
|
* - Order doesn't matter (all patterns are checked)
|
|
17
|
+
*
|
|
18
|
+
* v1.2.0 - Added 40+ new patterns for 2025-2026 services
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
export const SECRET_PATTERNS = [
|
|
@@ -49,27 +52,69 @@ export const SECRET_PATTERNS = [
|
|
|
49
52
|
severity: 'critical',
|
|
50
53
|
description: 'GitHub App tokens have installation-level access to repositories.'
|
|
51
54
|
},
|
|
55
|
+
{
|
|
56
|
+
name: 'GitHub Fine-Grained PAT',
|
|
57
|
+
pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
|
|
58
|
+
severity: 'critical',
|
|
59
|
+
description: 'GitHub fine-grained PATs can access repositories with scoped permissions.'
|
|
60
|
+
},
|
|
52
61
|
{
|
|
53
62
|
name: 'Stripe Live Secret Key',
|
|
54
63
|
pattern: /sk_live_[a-zA-Z0-9]{24,}/g,
|
|
55
64
|
severity: 'critical',
|
|
56
65
|
description: 'Stripe live keys can process real payments and access customer data.'
|
|
57
66
|
},
|
|
58
|
-
{
|
|
59
|
-
name: 'Stripe Live Publishable Key',
|
|
60
|
-
pattern: /pk_live_[a-zA-Z0-9]{24,}/g,
|
|
61
|
-
severity: 'high',
|
|
62
|
-
description: 'Stripe publishable keys are less sensitive but should not be in server code.'
|
|
63
|
-
},
|
|
64
67
|
{
|
|
65
68
|
name: 'Private Key Block',
|
|
66
69
|
pattern: /-----BEGIN\s+(RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g,
|
|
67
70
|
severity: 'critical',
|
|
68
71
|
description: 'Private keys enable impersonation and decryption. Never commit these.'
|
|
69
72
|
},
|
|
73
|
+
{
|
|
74
|
+
name: 'PlanetScale Password',
|
|
75
|
+
pattern: /pscale_pw_[a-zA-Z0-9_-]{32,}/g,
|
|
76
|
+
severity: 'critical',
|
|
77
|
+
description: 'PlanetScale passwords grant database access. Keep in environment variables.'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'PlanetScale OAuth Token',
|
|
81
|
+
pattern: /pscale_oauth_[a-zA-Z0-9_-]{32,}/g,
|
|
82
|
+
severity: 'critical',
|
|
83
|
+
description: 'PlanetScale OAuth tokens can manage your database branches and schema.'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Clerk Secret Key',
|
|
87
|
+
pattern: /sk_live_[a-zA-Z0-9]{27,}/g,
|
|
88
|
+
severity: 'critical',
|
|
89
|
+
description: 'Clerk secret keys grant full access to your auth system. Never expose in frontend.'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Doppler Service Token',
|
|
93
|
+
pattern: /dp\.st\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{32,}/g,
|
|
94
|
+
severity: 'critical',
|
|
95
|
+
description: 'Doppler service tokens grant access to your secrets. Ironic if leaked!'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'HashiCorp Vault Token',
|
|
99
|
+
pattern: /hvs\.[a-zA-Z0-9_-]{24,}/g,
|
|
100
|
+
severity: 'critical',
|
|
101
|
+
description: 'HashiCorp Vault tokens grant access to your secrets.'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'Neon Database Connection String',
|
|
105
|
+
pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@[^.]+\.neon\.tech/g,
|
|
106
|
+
severity: 'critical',
|
|
107
|
+
description: 'Neon Postgres connection strings contain database credentials.'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'MongoDB Atlas Connection String',
|
|
111
|
+
pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@[^.]+\.mongodb\.net/g,
|
|
112
|
+
severity: 'critical',
|
|
113
|
+
description: 'MongoDB Atlas connection strings contain database credentials.'
|
|
114
|
+
},
|
|
70
115
|
|
|
71
116
|
// =========================================================================
|
|
72
|
-
// HIGH:
|
|
117
|
+
// HIGH: AI/ML Provider Keys (2025-2026)
|
|
73
118
|
// =========================================================================
|
|
74
119
|
{
|
|
75
120
|
name: 'OpenAI API Key',
|
|
@@ -77,12 +122,70 @@ export const SECRET_PATTERNS = [
|
|
|
77
122
|
severity: 'high',
|
|
78
123
|
description: 'OpenAI keys can rack up API charges and access your usage history.'
|
|
79
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: 'OpenAI Project Key',
|
|
127
|
+
pattern: /sk-proj-[a-zA-Z0-9_-]{48,}/g,
|
|
128
|
+
severity: 'high',
|
|
129
|
+
description: 'OpenAI project keys grant access to specific project resources.'
|
|
130
|
+
},
|
|
80
131
|
{
|
|
81
132
|
name: 'Anthropic API Key',
|
|
82
|
-
pattern: /sk-ant-[a-zA-Z0-
|
|
133
|
+
pattern: /sk-ant-[a-zA-Z0-9_-]{32,}/g,
|
|
83
134
|
severity: 'high',
|
|
84
135
|
description: 'Anthropic API keys grant access to Claude and your usage quota.'
|
|
85
136
|
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Google AI (Gemini) API Key',
|
|
139
|
+
pattern: /AIzaSy[a-zA-Z0-9_-]{33}/g,
|
|
140
|
+
severity: 'high',
|
|
141
|
+
description: 'Google AI API keys grant access to Gemini and other Google AI services.'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'Replicate API Token',
|
|
145
|
+
pattern: /r8_[a-zA-Z0-9]{37}/g,
|
|
146
|
+
severity: 'high',
|
|
147
|
+
description: 'Replicate tokens can run AI models and incur charges on your account.'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'Hugging Face Token',
|
|
151
|
+
pattern: /hf_[a-zA-Z0-9]{34}/g,
|
|
152
|
+
severity: 'high',
|
|
153
|
+
description: 'Hugging Face tokens grant access to models, datasets, and Inference API.'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'Perplexity API Key',
|
|
157
|
+
pattern: /pplx-[a-f0-9]{48}/g,
|
|
158
|
+
severity: 'high',
|
|
159
|
+
description: 'Perplexity API keys can access their search-augmented AI models.'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Groq API Key',
|
|
163
|
+
pattern: /gsk_[a-zA-Z0-9]{52}/g,
|
|
164
|
+
severity: 'high',
|
|
165
|
+
description: 'Groq API keys provide access to fast LLM inference.'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'Cohere API Key',
|
|
169
|
+
pattern: /(?:cohere|COHERE)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{40})["']?/gi,
|
|
170
|
+
severity: 'high',
|
|
171
|
+
description: 'Cohere API keys grant access to their NLP models.'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'Mistral API Key',
|
|
175
|
+
pattern: /(?:mistral|MISTRAL)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{32})["']?/gi,
|
|
176
|
+
severity: 'high',
|
|
177
|
+
description: 'Mistral AI API keys can access their language models.'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'Together AI API Key',
|
|
181
|
+
pattern: /(?:together|TOGETHER)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{64})["']?/gi,
|
|
182
|
+
severity: 'high',
|
|
183
|
+
description: 'Together AI keys grant access to open-source model hosting.'
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// =========================================================================
|
|
187
|
+
// HIGH: Communication & Messaging
|
|
188
|
+
// =========================================================================
|
|
86
189
|
{
|
|
87
190
|
name: 'Slack Token',
|
|
88
191
|
pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/g,
|
|
@@ -108,11 +211,15 @@ export const SECRET_PATTERNS = [
|
|
|
108
211
|
description: 'Discord bot tokens grant full control over your bot.'
|
|
109
212
|
},
|
|
110
213
|
{
|
|
111
|
-
name: '
|
|
112
|
-
pattern: /
|
|
214
|
+
name: 'Telegram Bot Token',
|
|
215
|
+
pattern: /[0-9]{8,10}:[a-zA-Z0-9_-]{35}/g,
|
|
113
216
|
severity: 'high',
|
|
114
|
-
description: '
|
|
217
|
+
description: 'Telegram bot tokens grant full control over your bot.'
|
|
115
218
|
},
|
|
219
|
+
|
|
220
|
+
// =========================================================================
|
|
221
|
+
// HIGH: Email Services
|
|
222
|
+
// =========================================================================
|
|
116
223
|
{
|
|
117
224
|
name: 'SendGrid API Key',
|
|
118
225
|
pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
|
|
@@ -125,6 +232,44 @@ export const SECRET_PATTERNS = [
|
|
|
125
232
|
severity: 'high',
|
|
126
233
|
description: 'Mailgun keys can send emails and access logs.'
|
|
127
234
|
},
|
|
235
|
+
{
|
|
236
|
+
name: 'Resend API Key',
|
|
237
|
+
pattern: /re_[a-zA-Z0-9]{32,}/g,
|
|
238
|
+
severity: 'high',
|
|
239
|
+
description: 'Resend API keys can send emails from your account and access logs.'
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'Postmark Server Token',
|
|
243
|
+
pattern: /(?:postmark|POSTMARK)[_-]?(?:server[_-]?)?token["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
|
|
244
|
+
severity: 'high',
|
|
245
|
+
description: 'Postmark tokens can send emails from your account.'
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'Mailchimp API Key',
|
|
249
|
+
pattern: /[a-f0-9]{32}-us[0-9]{1,2}/g,
|
|
250
|
+
severity: 'high',
|
|
251
|
+
description: 'Mailchimp API keys can access your audience and send campaigns.'
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// =========================================================================
|
|
255
|
+
// HIGH: SMS & Phone
|
|
256
|
+
// =========================================================================
|
|
257
|
+
{
|
|
258
|
+
name: 'Twilio API Key',
|
|
259
|
+
pattern: /SK[a-f0-9]{32}/g,
|
|
260
|
+
severity: 'high',
|
|
261
|
+
description: 'Twilio keys can send SMS/calls and access account data.'
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'Twilio Account SID',
|
|
265
|
+
pattern: /AC[a-f0-9]{32}/g,
|
|
266
|
+
severity: 'medium',
|
|
267
|
+
description: 'Twilio Account SIDs identify your account. Usually paired with auth token.'
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// =========================================================================
|
|
271
|
+
// HIGH: Databases & Backend Services
|
|
272
|
+
// =========================================================================
|
|
128
273
|
{
|
|
129
274
|
name: 'Firebase/Google Service Account',
|
|
130
275
|
pattern: /"type":\s*"service_account"/g,
|
|
@@ -137,6 +282,34 @@ export const SECRET_PATTERNS = [
|
|
|
137
282
|
severity: 'high',
|
|
138
283
|
description: 'Supabase service role keys bypass Row Level Security. Keep server-side only.'
|
|
139
284
|
},
|
|
285
|
+
{
|
|
286
|
+
name: 'Upstash Redis REST Token',
|
|
287
|
+
pattern: /AX[a-zA-Z0-9]{34,}/g,
|
|
288
|
+
severity: 'high',
|
|
289
|
+
description: 'Upstash Redis tokens grant access to your serverless Redis database.'
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'Upstash QStash Token',
|
|
293
|
+
pattern: /qstash_[a-zA-Z0-9]{32,}/g,
|
|
294
|
+
severity: 'high',
|
|
295
|
+
description: 'Upstash QStash tokens can schedule and manage message queues.'
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'Turso Database URL',
|
|
299
|
+
pattern: /libsql:\/\/[^.]+\.turso\.io/g,
|
|
300
|
+
severity: 'high',
|
|
301
|
+
description: 'Turso database URLs. Check for embedded auth tokens in full connection string.'
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'Convex Deployment URL',
|
|
305
|
+
pattern: /https:\/\/[a-z]+-[a-z]+-[0-9]+\.convex\.cloud/g,
|
|
306
|
+
severity: 'medium',
|
|
307
|
+
description: 'Convex deployment URLs identify your backend. Check for paired secrets.'
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// =========================================================================
|
|
311
|
+
// HIGH: Hosting & Deployment
|
|
312
|
+
// =========================================================================
|
|
140
313
|
{
|
|
141
314
|
name: 'Vercel Token',
|
|
142
315
|
pattern: /vercel_[a-zA-Z0-9]{24}/gi,
|
|
@@ -161,19 +334,167 @@ export const SECRET_PATTERNS = [
|
|
|
161
334
|
severity: 'high',
|
|
162
335
|
description: 'DigitalOcean tokens can manage droplets and resources.'
|
|
163
336
|
},
|
|
337
|
+
{
|
|
338
|
+
name: 'Render API Key',
|
|
339
|
+
pattern: /rnd_[a-zA-Z0-9]{32,}/g,
|
|
340
|
+
severity: 'high',
|
|
341
|
+
description: 'Render API keys can manage your services and deployments.'
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'Fly.io Token',
|
|
345
|
+
pattern: /FlyV1\s+[a-zA-Z0-9_-]{43}/g,
|
|
346
|
+
severity: 'high',
|
|
347
|
+
description: 'Fly.io tokens can deploy and manage your applications.'
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'Railway Token',
|
|
351
|
+
pattern: /(?:railway|RAILWAY)[_-]?token["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
|
|
352
|
+
severity: 'high',
|
|
353
|
+
description: 'Railway API tokens can manage your services.'
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: 'Netlify Personal Access Token',
|
|
357
|
+
pattern: /nfp_[a-zA-Z0-9]{40}/g,
|
|
358
|
+
severity: 'high',
|
|
359
|
+
description: 'Netlify PATs can manage sites and deploys.'
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'Cloudflare API Token',
|
|
363
|
+
pattern: /(?:cloudflare|CF)[_-]?(?:api[_-]?)?token["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{40})["']?/gi,
|
|
364
|
+
severity: 'high',
|
|
365
|
+
description: 'Cloudflare API tokens can manage DNS, workers, and other services.'
|
|
366
|
+
},
|
|
164
367
|
|
|
165
368
|
// =========================================================================
|
|
166
|
-
//
|
|
369
|
+
// HIGH: Auth Providers
|
|
167
370
|
// =========================================================================
|
|
168
371
|
{
|
|
169
|
-
name: '
|
|
170
|
-
pattern: /[
|
|
372
|
+
name: 'Clerk Publishable Key (Live)',
|
|
373
|
+
pattern: /pk_live_[a-zA-Z0-9]{27,}/g,
|
|
374
|
+
severity: 'medium',
|
|
375
|
+
description: 'Clerk publishable keys are meant for frontend but verify it\'s intentional.'
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'Clerk Test Secret Key',
|
|
379
|
+
pattern: /sk_test_[a-zA-Z0-9]{27,}/g,
|
|
380
|
+
severity: 'medium',
|
|
381
|
+
description: 'Clerk test keys are lower risk but should still be in environment variables.'
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'Auth0 Domain with Credentials',
|
|
385
|
+
pattern: /https:\/\/[^.]+\.auth0\.com.*client_secret/gi,
|
|
386
|
+
severity: 'critical',
|
|
387
|
+
description: 'Auth0 URLs with embedded client secrets should never be in code.'
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'Supabase Anon Key in Code',
|
|
391
|
+
pattern: /(?:supabase|SUPABASE)[_-]?(?:anon[_-]?)?key["']?\s*[:=]\s*["']?(eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)["']?/gi,
|
|
392
|
+
severity: 'medium',
|
|
393
|
+
description: 'Supabase anon keys. Safe for frontend but verify RLS is enabled.'
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
// =========================================================================
|
|
397
|
+
// HIGH: Productivity & SaaS
|
|
398
|
+
// =========================================================================
|
|
399
|
+
{
|
|
400
|
+
name: 'Linear API Key',
|
|
401
|
+
pattern: /lin_api_[a-zA-Z0-9]{40}/g,
|
|
402
|
+
severity: 'high',
|
|
403
|
+
description: 'Linear API keys can access your project management data.'
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'Notion API Key',
|
|
407
|
+
pattern: /secret_[a-zA-Z0-9]{43}/g,
|
|
408
|
+
severity: 'high',
|
|
409
|
+
description: 'Notion API keys can access and modify your workspace content.'
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'Airtable API Key',
|
|
413
|
+
pattern: /pat[a-zA-Z0-9]{14}\.[a-f0-9]{64}/g,
|
|
414
|
+
severity: 'high',
|
|
415
|
+
description: 'Airtable personal access tokens grant access to your bases.'
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'Figma Personal Access Token',
|
|
419
|
+
pattern: /figd_[a-zA-Z0-9_-]{40,}/g,
|
|
420
|
+
severity: 'high',
|
|
421
|
+
description: 'Figma PATs can access your design files and projects.'
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
// =========================================================================
|
|
425
|
+
// HIGH: Payments (Additional)
|
|
426
|
+
// =========================================================================
|
|
427
|
+
{
|
|
428
|
+
name: 'Stripe Test Secret Key',
|
|
429
|
+
pattern: /sk_test_[a-zA-Z0-9]{24,}/g,
|
|
430
|
+
severity: 'medium',
|
|
431
|
+
description: 'Stripe test keys are lower risk but should still be in environment variables.'
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'Stripe Live Publishable Key',
|
|
435
|
+
pattern: /pk_live_[a-zA-Z0-9]{24,}/g,
|
|
436
|
+
severity: 'medium',
|
|
437
|
+
description: 'Stripe publishable keys are meant for frontend but verify it\'s intentional.'
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'Stripe Webhook Secret',
|
|
441
|
+
pattern: /whsec_[a-zA-Z0-9]{32,}/g,
|
|
442
|
+
severity: 'high',
|
|
443
|
+
description: 'Stripe webhook secrets validate incoming webhooks. Keep server-side only.'
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'Lemon Squeezy API Key',
|
|
447
|
+
pattern: /(?:lemon|LEMON)[_-]?(?:squeezy|SQUEEZY)?[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
|
|
448
|
+
severity: 'high',
|
|
449
|
+
description: 'Lemon Squeezy API keys can manage your store and orders.'
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'Paddle API Key',
|
|
453
|
+
pattern: /(?:paddle|PADDLE)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{64})["']?/gi,
|
|
454
|
+
severity: 'high',
|
|
455
|
+
description: 'Paddle API keys can manage your subscriptions and payments.'
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
// =========================================================================
|
|
459
|
+
// HIGH: Analytics & Monitoring
|
|
460
|
+
// =========================================================================
|
|
461
|
+
{
|
|
462
|
+
name: 'Sentry DSN',
|
|
463
|
+
pattern: /https:\/\/[a-f0-9]{32}@[a-z0-9]+\.ingest\.sentry\.io\/[0-9]+/g,
|
|
464
|
+
severity: 'medium',
|
|
465
|
+
description: 'Sentry DSNs are semi-public but contain project identifiers.'
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'PostHog API Key',
|
|
469
|
+
pattern: /phc_[a-zA-Z0-9]{32,}/g,
|
|
470
|
+
severity: 'medium',
|
|
471
|
+
description: 'PostHog project API keys. Usually safe in frontend but verify.'
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: 'New Relic API Key',
|
|
475
|
+
pattern: /NRAK-[A-Z0-9]{27}/g,
|
|
476
|
+
severity: 'high',
|
|
477
|
+
description: 'New Relic API keys can access your monitoring data and configurations.'
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'Datadog API Key',
|
|
481
|
+
pattern: /(?:datadog|DD)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{32})["']?/gi,
|
|
482
|
+
severity: 'high',
|
|
483
|
+
description: 'Datadog API keys can access and send monitoring data.'
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
// =========================================================================
|
|
487
|
+
// MEDIUM: Generic patterns (may have false positives)
|
|
488
|
+
// =========================================================================
|
|
489
|
+
{
|
|
490
|
+
name: 'Generic API Key Assignment',
|
|
491
|
+
pattern: /["']?(?:api[_-]?key|apikey)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/gi,
|
|
171
492
|
severity: 'medium',
|
|
172
493
|
description: 'Hardcoded API keys should be moved to environment variables.'
|
|
173
494
|
},
|
|
174
495
|
{
|
|
175
|
-
name: 'Generic Secret',
|
|
176
|
-
pattern: /["']?(?:secret|secret[_-]?key)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{
|
|
496
|
+
name: 'Generic Secret Assignment',
|
|
497
|
+
pattern: /["']?(?:secret|secret[_-]?key)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/gi,
|
|
177
498
|
severity: 'medium',
|
|
178
499
|
description: 'Hardcoded secrets should be moved to environment variables.'
|
|
179
500
|
},
|
|
@@ -190,22 +511,22 @@ export const SECRET_PATTERNS = [
|
|
|
190
511
|
description: 'Database URLs with embedded passwords expose your database.'
|
|
191
512
|
},
|
|
192
513
|
{
|
|
193
|
-
name: 'Bearer Token',
|
|
514
|
+
name: 'Bearer Token in Code',
|
|
194
515
|
pattern: /["']Bearer\s+[a-zA-Z0-9_\-\.=]{20,}["']/gi,
|
|
195
516
|
severity: 'medium',
|
|
196
517
|
description: 'Hardcoded bearer tokens should not be in source code.'
|
|
197
518
|
},
|
|
198
|
-
{
|
|
199
|
-
name: 'JWT Token',
|
|
200
|
-
pattern: /["']?jwt["']?\s*[:=]\s*["']?(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)["']?/gi,
|
|
201
|
-
severity: 'medium',
|
|
202
|
-
description: 'JWTs in source code may be test tokens, but verify they\'re not production.'
|
|
203
|
-
},
|
|
204
519
|
{
|
|
205
520
|
name: 'Basic Auth Header',
|
|
206
|
-
pattern: /["']Basic\s+[A-Za-z0-9+/=]{
|
|
521
|
+
pattern: /["']Basic\s+[A-Za-z0-9+/=]{20,}["']/gi,
|
|
207
522
|
severity: 'medium',
|
|
208
523
|
description: 'Basic auth headers contain base64-encoded credentials.'
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'Private Key in Environment Variable',
|
|
527
|
+
pattern: /PRIVATE[_-]?KEY["']?\s*[:=]\s*["']([^"']+)["']/gi,
|
|
528
|
+
severity: 'high',
|
|
529
|
+
description: 'Private keys should be loaded from files, not hardcoded.'
|
|
209
530
|
}
|
|
210
531
|
];
|
|
211
532
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// FIREBASE FIRESTORE SECURITY RULES TEMPLATES
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Copy these rules to your Firebase Console > Firestore > Rules
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS MATTERS:
|
|
8
|
+
// - Default test mode rules allow ANYONE to read/write ALL data
|
|
9
|
+
// - Test mode is the #1 cause of Firebase data breaches
|
|
10
|
+
// - 40% of Firebase apps have misconfigured security rules
|
|
11
|
+
//
|
|
12
|
+
// HOW TO USE:
|
|
13
|
+
// 1. Go to Firebase Console > Firestore Database > Rules
|
|
14
|
+
// 2. Replace the default rules with these templates
|
|
15
|
+
// 3. Customize for your data structure
|
|
16
|
+
// 4. Publish and test thoroughly
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
rules_version = '2';
|
|
21
|
+
service cloud.firestore {
|
|
22
|
+
match /databases/{database}/documents {
|
|
23
|
+
|
|
24
|
+
// =========================================================================
|
|
25
|
+
// HELPER FUNCTIONS
|
|
26
|
+
// =========================================================================
|
|
27
|
+
|
|
28
|
+
// Check if user is authenticated
|
|
29
|
+
function isAuthenticated() {
|
|
30
|
+
return request.auth != null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get the authenticated user's ID
|
|
34
|
+
function userId() {
|
|
35
|
+
return request.auth.uid;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if user owns a document (document has a userId field)
|
|
39
|
+
function isOwner() {
|
|
40
|
+
return isAuthenticated() && resource.data.userId == userId();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if user is creating a document with their own userId
|
|
44
|
+
function isCreatingOwn() {
|
|
45
|
+
return isAuthenticated() && request.resource.data.userId == userId();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if user has a specific role (requires a users collection with role field)
|
|
49
|
+
function hasRole(role) {
|
|
50
|
+
return isAuthenticated() &&
|
|
51
|
+
get(/databases/$(database)/documents/users/$(userId())).data.role == role;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if user is admin
|
|
55
|
+
function isAdmin() {
|
|
56
|
+
return hasRole('admin');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate that required fields exist
|
|
60
|
+
function hasRequiredFields(fields) {
|
|
61
|
+
return request.resource.data.keys().hasAll(fields);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if only allowed fields are being modified
|
|
65
|
+
function onlyUpdating(fields) {
|
|
66
|
+
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =========================================================================
|
|
70
|
+
// PATTERN 1: USER PROFILES (User owns their profile)
|
|
71
|
+
// =========================================================================
|
|
72
|
+
|
|
73
|
+
match /users/{userId} {
|
|
74
|
+
// Users can read their own profile
|
|
75
|
+
allow read: if isAuthenticated() && request.auth.uid == userId;
|
|
76
|
+
|
|
77
|
+
// Users can create their own profile
|
|
78
|
+
allow create: if isAuthenticated()
|
|
79
|
+
&& request.auth.uid == userId
|
|
80
|
+
&& hasRequiredFields(['email', 'createdAt'])
|
|
81
|
+
&& request.resource.data.createdAt == request.time;
|
|
82
|
+
|
|
83
|
+
// Users can update their own profile (except role)
|
|
84
|
+
allow update: if isAuthenticated()
|
|
85
|
+
&& request.auth.uid == userId
|
|
86
|
+
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
|
|
87
|
+
|
|
88
|
+
// Only admins can delete users
|
|
89
|
+
allow delete: if isAdmin();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =========================================================================
|
|
93
|
+
// PATTERN 2: PRIVATE DATA (User's own data)
|
|
94
|
+
// =========================================================================
|
|
95
|
+
|
|
96
|
+
match /private/{userId}/{document=**} {
|
|
97
|
+
// Users can only access their own private data
|
|
98
|
+
allow read, write: if isAuthenticated() && request.auth.uid == userId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// PATTERN 3: POSTS/CONTENT (Public read, authenticated write)
|
|
103
|
+
// =========================================================================
|
|
104
|
+
|
|
105
|
+
match /posts/{postId} {
|
|
106
|
+
// Anyone can read published posts
|
|
107
|
+
allow read: if resource.data.published == true
|
|
108
|
+
|| (isAuthenticated() && resource.data.authorId == userId());
|
|
109
|
+
|
|
110
|
+
// Authenticated users can create posts
|
|
111
|
+
allow create: if isAuthenticated()
|
|
112
|
+
&& request.resource.data.authorId == userId()
|
|
113
|
+
&& hasRequiredFields(['title', 'content', 'authorId', 'createdAt'])
|
|
114
|
+
&& request.resource.data.createdAt == request.time;
|
|
115
|
+
|
|
116
|
+
// Authors can update their own posts
|
|
117
|
+
allow update: if isAuthenticated()
|
|
118
|
+
&& resource.data.authorId == userId()
|
|
119
|
+
&& request.resource.data.authorId == userId(); // Can't change author
|
|
120
|
+
|
|
121
|
+
// Authors and admins can delete
|
|
122
|
+
allow delete: if isAuthenticated()
|
|
123
|
+
&& (resource.data.authorId == userId() || isAdmin());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =========================================================================
|
|
127
|
+
// PATTERN 4: ORGANIZATION/TEAM DATA
|
|
128
|
+
// =========================================================================
|
|
129
|
+
|
|
130
|
+
match /organizations/{orgId} {
|
|
131
|
+
// Function to check org membership
|
|
132
|
+
function isOrgMember() {
|
|
133
|
+
return isAuthenticated() &&
|
|
134
|
+
exists(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId()));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isOrgAdmin() {
|
|
138
|
+
return isAuthenticated() &&
|
|
139
|
+
get(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId())).data.role == 'admin';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Org members can read org data
|
|
143
|
+
allow read: if isOrgMember();
|
|
144
|
+
|
|
145
|
+
// Only org admins can update org settings
|
|
146
|
+
allow update: if isOrgAdmin();
|
|
147
|
+
|
|
148
|
+
// Nested collection: org members
|
|
149
|
+
match /members/{memberId} {
|
|
150
|
+
allow read: if isOrgMember();
|
|
151
|
+
allow write: if isOrgAdmin();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Nested collection: org projects
|
|
155
|
+
match /projects/{projectId} {
|
|
156
|
+
allow read: if isOrgMember();
|
|
157
|
+
allow create: if isOrgMember()
|
|
158
|
+
&& request.resource.data.createdBy == userId();
|
|
159
|
+
allow update: if isOrgMember();
|
|
160
|
+
allow delete: if isOrgAdmin();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// PATTERN 5: RATE LIMITING (Using timestamps)
|
|
166
|
+
// =========================================================================
|
|
167
|
+
|
|
168
|
+
match /rateLimited/{docId} {
|
|
169
|
+
// Allow creation only if user hasn't created one in the last minute
|
|
170
|
+
allow create: if isAuthenticated()
|
|
171
|
+
&& (!exists(/databases/$(database)/documents/rateLimited/$(userId()))
|
|
172
|
+
|| get(/databases/$(database)/documents/rateLimited/$(userId())).data.lastAction < request.time - duration.value(1, 'm'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// =========================================================================
|
|
176
|
+
// PATTERN 6: READ-ONLY PUBLIC DATA
|
|
177
|
+
// =========================================================================
|
|
178
|
+
|
|
179
|
+
match /public/{document=**} {
|
|
180
|
+
// Anyone can read public data
|
|
181
|
+
allow read: if true;
|
|
182
|
+
|
|
183
|
+
// Only admins can write
|
|
184
|
+
allow write: if isAdmin();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =========================================================================
|
|
188
|
+
// ANTI-PATTERNS: NEVER DO THIS
|
|
189
|
+
// =========================================================================
|
|
190
|
+
|
|
191
|
+
// BAD: Allows anyone to read/write everything
|
|
192
|
+
// match /{document=**} {
|
|
193
|
+
// allow read, write: if true;
|
|
194
|
+
// }
|
|
195
|
+
|
|
196
|
+
// BAD: Test mode rules (auto-generated, REMOVE before production)
|
|
197
|
+
// match /{document=**} {
|
|
198
|
+
// allow read, write: if request.time < timestamp.date(2024, 12, 31);
|
|
199
|
+
// }
|
|
200
|
+
|
|
201
|
+
// BAD: Checking auth but not ownership
|
|
202
|
+
// match /users/{userId} {
|
|
203
|
+
// allow read, write: if request.auth != null; // Any user can access any user!
|
|
204
|
+
// }
|
|
205
|
+
|
|
206
|
+
// =========================================================================
|
|
207
|
+
// DEFAULT: DENY ALL (Safety net)
|
|
208
|
+
// =========================================================================
|
|
209
|
+
|
|
210
|
+
// Deny access to any collection not explicitly defined above
|
|
211
|
+
match /{document=**} {
|
|
212
|
+
allow read, write: if false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|