Skip to content

Commit f478219

Browse files
committed
fix: narrow valid package types and harden snapshot upload
- Remove mas, pip, gem, cargo, go from validTypes — only formula, cask, tap, npm are supported by both CLI and dashboard - Add Content-Length pre-check (413) before parsing snapshot body - Keep post-parse fallback check for spoofed Content-Length - Add regression test asserting removed types are rejected - Update JSDoc to match new allowlist
1 parent a9630c5 commit f478219

File tree

3 files changed

+20
-7
lines changed

3 files changed

+20
-7
lines changed

src/lib/server/validation.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,8 @@ describe('validatePackages', () => {
292292
expect(result.valid).toBe(true);
293293
});
294294

295-
it('should accept packages with slashes (go modules, npm scopes)', () => {
295+
it('should accept packages with slashes (npm scopes)', () => {
296296
const packages = [
297-
{ name: 'github.com/user/repo', type: 'go' },
298297
{ name: '@org/package', type: 'npm' }
299298
];
300299
const result = validatePackages(packages);
@@ -417,7 +416,7 @@ describe('validatePackages', () => {
417416
});
418417

419418
it('should accept all valid package types', () => {
420-
const types = ['formula', 'cask', 'tap', 'mas', 'npm', 'pip', 'gem', 'cargo', 'go'];
419+
const types = ['formula', 'cask', 'tap', 'npm'];
421420
for (const type of types) {
422421
const packages = [{ name: 'valid-package', type }];
423422
const result = validatePackages(packages);
@@ -426,6 +425,14 @@ describe('validatePackages', () => {
426425
}
427426
});
428427

428+
it('should reject previously-allowed types that are no longer valid', () => {
429+
for (const type of ['pip', 'gem', 'cargo', 'mas', 'go']) {
430+
const result = validatePackages([{ name: 'pkg', type }]);
431+
expect(result.valid).toBe(false);
432+
expect(result.error).toContain('Invalid package type');
433+
}
434+
});
435+
429436
it('should reject description longer than 500 characters', () => {
430437
const packages = [
431438
{

src/lib/server/validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ interface Package {
140140

141141
/** Validates packages array: prevents shell injection in package names.
142142
* Package names must match standard package manager formats (alphanumeric, hyphens, underscores, dots, slashes for scoped packages).
143-
* Types must be: formula, cask, tap, mas, npm, pip, gem, cargo, go.
143+
* Types must be: formula, cask, tap, npm.
144144
* Maximum 500 packages per config. */
145145
export function validatePackages(packages: unknown): ValidationResult {
146146
if (!packages) {
@@ -155,7 +155,7 @@ export function validatePackages(packages: unknown): ValidationResult {
155155
return { valid: false, error: 'Maximum 500 packages allowed' };
156156
}
157157

158-
const validTypes = ['formula', 'cask', 'tap', 'mas', 'npm', 'pip', 'gem', 'cargo', 'go'];
158+
const validTypes = ['formula', 'cask', 'tap', 'npm'];
159159

160160
for (let i = 0; i < packages.length; i++) {
161161
const pkg = packages[i];

src/routes/api/configs/from-snapshot/+server.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const POST: RequestHandler = async ({ platform, cookies, request }) => {
1616
return json({ error: 'Rate limit exceeded' }, { status: 429, headers: { 'Retry-After': String(Math.ceil(rl.retryAfter! / 1000)) } });
1717
}
1818

19+
const contentLength = parseInt(request.headers.get('content-length') ?? '0', 10);
20+
if (contentLength > 1_048_576) {
21+
return json({ error: 'Snapshot payload too large (max 1MB)' }, { status: 413 });
22+
}
23+
1924
let body;
2025
try {
2126
body = await request.json();
@@ -33,9 +38,10 @@ export const POST: RequestHandler = async ({ platform, cookies, request }) => {
3338
const validVisibilities = ['public', 'unlisted', 'private'];
3439
const validVisibility = validVisibilities.includes(visibility) ? visibility : 'unlisted';
3540

41+
// Post-parse size check as a fallback (Content-Length can be spoofed or absent).
3642
const snapshotSize = JSON.stringify(snapshot).length;
37-
if (snapshotSize > 100000) {
38-
return json({ error: 'Snapshot payload too large (max 100KB)' }, { status: 400 });
43+
if (snapshotSize > 1_048_576) {
44+
return json({ error: 'Snapshot payload too large (max 1MB)' }, { status: 413 });
3945
}
4046

4147
const base_preset = snapshot.matched_preset || 'developer';

0 commit comments

Comments
 (0)