ship-safe 1.0.1 → 3.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/README.md +281 -23
- 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 +44 -2
- package/cli/commands/fix.js +216 -0
- package/cli/commands/guard.js +297 -0
- package/cli/commands/mcp.js +303 -0
- package/cli/commands/scan.js +231 -39
- package/cli/utils/entropy.js +126 -0
- package/cli/utils/output.js +10 -1
- package/cli/utils/patterns.js +376 -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/ship-safeignore-template +50 -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 +11 -2
- 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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Configuration Examples
|
|
3
|
+
* ===========================
|
|
4
|
+
*
|
|
5
|
+
* Cross-Origin Resource Sharing (CORS) configurations for common frameworks.
|
|
6
|
+
*
|
|
7
|
+
* WHY CORS MATTERS:
|
|
8
|
+
* - Prevents unauthorized websites from calling your API
|
|
9
|
+
* - Blocks cross-site request forgery (CSRF) attacks
|
|
10
|
+
* - Controls which domains can access your resources
|
|
11
|
+
*
|
|
12
|
+
* COMMON MISTAKES:
|
|
13
|
+
* - Setting origin: '*' in production (allows any site)
|
|
14
|
+
* - Not validating origin properly (regex bypass)
|
|
15
|
+
* - Forgetting credentials handling
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// NEXT.JS API ROUTES
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Next.js API route with CORS
|
|
24
|
+
* Use in: pages/api/*.ts or app/api/*/route.ts
|
|
25
|
+
*/
|
|
26
|
+
export const nextjsCorsConfig = `
|
|
27
|
+
// next.config.js - Basic headers approach
|
|
28
|
+
/** @type {import('next').NextConfig} */
|
|
29
|
+
const nextConfig = {
|
|
30
|
+
async headers() {
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
source: '/api/:path*',
|
|
34
|
+
headers: [
|
|
35
|
+
{
|
|
36
|
+
key: 'Access-Control-Allow-Origin',
|
|
37
|
+
// CHANGE THIS to your frontend domain
|
|
38
|
+
value: process.env.FRONTEND_URL || 'https://yourapp.com',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'Access-Control-Allow-Methods',
|
|
42
|
+
value: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'Access-Control-Allow-Headers',
|
|
46
|
+
value: 'Content-Type, Authorization',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'Access-Control-Allow-Credentials',
|
|
50
|
+
value: 'true',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
module.exports = nextConfig;
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Next.js API route handler with manual CORS
|
|
63
|
+
*/
|
|
64
|
+
export const nextjsRouteHandler = `
|
|
65
|
+
// app/api/example/route.ts
|
|
66
|
+
import { NextResponse } from 'next/server';
|
|
67
|
+
import type { NextRequest } from 'next/server';
|
|
68
|
+
|
|
69
|
+
// Allowed origins - ADD YOUR DOMAINS HERE
|
|
70
|
+
const ALLOWED_ORIGINS = [
|
|
71
|
+
'https://yourapp.com',
|
|
72
|
+
'https://www.yourapp.com',
|
|
73
|
+
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
|
|
74
|
+
].filter(Boolean) as string[];
|
|
75
|
+
|
|
76
|
+
function getCorsHeaders(origin: string | null) {
|
|
77
|
+
const headers: Record<string, string> = {
|
|
78
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
79
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
80
|
+
'Access-Control-Max-Age': '86400', // 24 hours
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Only allow specific origins
|
|
84
|
+
if (origin && ALLOWED_ORIGINS.includes(origin)) {
|
|
85
|
+
headers['Access-Control-Allow-Origin'] = origin;
|
|
86
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return headers;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle preflight requests
|
|
93
|
+
export async function OPTIONS(request: NextRequest) {
|
|
94
|
+
const origin = request.headers.get('origin');
|
|
95
|
+
return new NextResponse(null, {
|
|
96
|
+
status: 204,
|
|
97
|
+
headers: getCorsHeaders(origin),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function GET(request: NextRequest) {
|
|
102
|
+
const origin = request.headers.get('origin');
|
|
103
|
+
|
|
104
|
+
// Your logic here
|
|
105
|
+
const data = { message: 'Hello!' };
|
|
106
|
+
|
|
107
|
+
return NextResponse.json(data, {
|
|
108
|
+
headers: getCorsHeaders(origin),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function POST(request: NextRequest) {
|
|
113
|
+
const origin = request.headers.get('origin');
|
|
114
|
+
|
|
115
|
+
// Validate origin for mutations
|
|
116
|
+
if (origin && !ALLOWED_ORIGINS.includes(origin)) {
|
|
117
|
+
return new NextResponse('Forbidden', { status: 403 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Your logic here
|
|
121
|
+
const body = await request.json();
|
|
122
|
+
|
|
123
|
+
return NextResponse.json({ success: true }, {
|
|
124
|
+
headers: getCorsHeaders(origin),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// EXPRESS.JS
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
export const expressCorsConfig = `
|
|
134
|
+
// Express CORS configuration
|
|
135
|
+
import cors from 'cors';
|
|
136
|
+
import express from 'express';
|
|
137
|
+
|
|
138
|
+
const app = express();
|
|
139
|
+
|
|
140
|
+
// BAD: Allows any origin
|
|
141
|
+
// app.use(cors()); // DON'T DO THIS IN PRODUCTION
|
|
142
|
+
|
|
143
|
+
// GOOD: Specific origins
|
|
144
|
+
const ALLOWED_ORIGINS = [
|
|
145
|
+
'https://yourapp.com',
|
|
146
|
+
'https://www.yourapp.com',
|
|
147
|
+
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
|
|
150
|
+
const corsOptions = {
|
|
151
|
+
origin: function (origin, callback) {
|
|
152
|
+
// Allow requests with no origin (mobile apps, curl, etc.)
|
|
153
|
+
// Remove this if you want to block those too
|
|
154
|
+
if (!origin) {
|
|
155
|
+
return callback(null, true);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (ALLOWED_ORIGINS.includes(origin)) {
|
|
159
|
+
callback(null, true);
|
|
160
|
+
} else {
|
|
161
|
+
callback(new Error('CORS not allowed'));
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
credentials: true,
|
|
165
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
166
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
167
|
+
maxAge: 86400, // 24 hours
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
app.use(cors(corsOptions));
|
|
171
|
+
|
|
172
|
+
// Handle CORS errors
|
|
173
|
+
app.use((err, req, res, next) => {
|
|
174
|
+
if (err.message === 'CORS not allowed') {
|
|
175
|
+
return res.status(403).json({ error: 'Origin not allowed' });
|
|
176
|
+
}
|
|
177
|
+
next(err);
|
|
178
|
+
});
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// FASTIFY
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
export const fastifyCorsConfig = `
|
|
186
|
+
// Fastify CORS configuration
|
|
187
|
+
import Fastify from 'fastify';
|
|
188
|
+
import cors from '@fastify/cors';
|
|
189
|
+
|
|
190
|
+
const fastify = Fastify();
|
|
191
|
+
|
|
192
|
+
const ALLOWED_ORIGINS = [
|
|
193
|
+
'https://yourapp.com',
|
|
194
|
+
'https://www.yourapp.com',
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
await fastify.register(cors, {
|
|
198
|
+
origin: (origin, callback) => {
|
|
199
|
+
if (!origin || ALLOWED_ORIGINS.includes(origin)) {
|
|
200
|
+
callback(null, true);
|
|
201
|
+
} else {
|
|
202
|
+
callback(new Error('Not allowed'), false);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
credentials: true,
|
|
206
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
207
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
208
|
+
});
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
// =============================================================================
|
|
212
|
+
// HONO (EDGE)
|
|
213
|
+
// =============================================================================
|
|
214
|
+
|
|
215
|
+
export const honoCorsConfig = `
|
|
216
|
+
// Hono CORS configuration (for Cloudflare Workers, etc.)
|
|
217
|
+
import { Hono } from 'hono';
|
|
218
|
+
import { cors } from 'hono/cors';
|
|
219
|
+
|
|
220
|
+
const app = new Hono();
|
|
221
|
+
|
|
222
|
+
const ALLOWED_ORIGINS = [
|
|
223
|
+
'https://yourapp.com',
|
|
224
|
+
'https://www.yourapp.com',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
app.use('/*', cors({
|
|
228
|
+
origin: (origin) => {
|
|
229
|
+
if (ALLOWED_ORIGINS.includes(origin)) {
|
|
230
|
+
return origin;
|
|
231
|
+
}
|
|
232
|
+
return null; // Block the request
|
|
233
|
+
},
|
|
234
|
+
credentials: true,
|
|
235
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
236
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
237
|
+
maxAge: 86400,
|
|
238
|
+
}));
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
// =============================================================================
|
|
242
|
+
// VERCEL EDGE FUNCTIONS
|
|
243
|
+
// =============================================================================
|
|
244
|
+
|
|
245
|
+
export const vercelEdgeCors = `
|
|
246
|
+
// Vercel Edge Function with CORS
|
|
247
|
+
// api/example.ts
|
|
248
|
+
import type { NextRequest } from 'next/server';
|
|
249
|
+
|
|
250
|
+
export const config = {
|
|
251
|
+
runtime: 'edge',
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const ALLOWED_ORIGINS = [
|
|
255
|
+
'https://yourapp.com',
|
|
256
|
+
'https://www.yourapp.com',
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
export default async function handler(request: NextRequest) {
|
|
260
|
+
const origin = request.headers.get('origin') || '';
|
|
261
|
+
|
|
262
|
+
// Validate origin
|
|
263
|
+
const isAllowed = ALLOWED_ORIGINS.includes(origin);
|
|
264
|
+
|
|
265
|
+
// Handle preflight
|
|
266
|
+
if (request.method === 'OPTIONS') {
|
|
267
|
+
return new Response(null, {
|
|
268
|
+
status: 204,
|
|
269
|
+
headers: {
|
|
270
|
+
'Access-Control-Allow-Origin': isAllowed ? origin : '',
|
|
271
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
272
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
273
|
+
'Access-Control-Max-Age': '86400',
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Block non-allowed origins for mutations
|
|
279
|
+
if (request.method !== 'GET' && !isAllowed) {
|
|
280
|
+
return new Response('Forbidden', { status: 403 });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Your logic here
|
|
284
|
+
const data = { message: 'Hello from the edge!' };
|
|
285
|
+
|
|
286
|
+
return new Response(JSON.stringify(data), {
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
'Access-Control-Allow-Origin': isAllowed ? origin : '',
|
|
290
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// ANTI-PATTERNS
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
export const corsAntiPatterns = `
|
|
301
|
+
// CORS ANTI-PATTERNS - DON'T DO THESE
|
|
302
|
+
|
|
303
|
+
// 1. Wildcard origin (allows any site to call your API)
|
|
304
|
+
app.use(cors({ origin: '*' }));
|
|
305
|
+
|
|
306
|
+
// 2. Reflecting any origin (same problem as wildcard)
|
|
307
|
+
app.use(cors({
|
|
308
|
+
origin: (origin, callback) => callback(null, origin), // BAD!
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
// 3. Regex without anchors (can be bypassed)
|
|
312
|
+
// Attacker can use: https://yourapp.com.evil.com
|
|
313
|
+
const UNSAFE_PATTERN = /yourapp\\.com/; // BAD!
|
|
314
|
+
const SAFE_PATTERN = /^https:\\/\\/(www\\.)?yourapp\\.com$/; // GOOD
|
|
315
|
+
|
|
316
|
+
// 4. Not handling credentials properly
|
|
317
|
+
// If you need cookies/auth headers, you MUST specify exact origins
|
|
318
|
+
// Wildcard doesn't work with credentials
|
|
319
|
+
|
|
320
|
+
// 5. Long maxAge without reviewing
|
|
321
|
+
// If you change CORS config, users won't see it for maxAge seconds
|
|
322
|
+
`;
|