Skip to content

Commit 254c2ab

Browse files
author
teycir
committed
feat: enhance create-seal API with additional input validations
- Add validation for request size, keyB, IV, and unlock timestamp to ensure data integrity and prevent invalid submissions. Reorganize validation order for better error handling.
1 parent 0cc8108 commit 254c2ab

8 files changed

Lines changed: 522 additions & 61 deletions

File tree

app/api/create-seal/route.ts

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,92 @@
1-
import { NextRequest } from 'next/server';
2-
import { jsonResponse } from '@/lib/apiHandler';
3-
import { createAPIRoute } from '@/lib/routeHelper';
4-
import { validateFileSize, validateUnlockTime } from '@/lib/validation';
5-
import { ErrorCode, createErrorResponse } from '@/lib/errors';
6-
1+
import { NextRequest } from "next/server";
2+
import { jsonResponse } from "@/lib/apiHandler";
3+
import { createAPIRoute } from "@/lib/routeHelper";
4+
import {
5+
validateFileSize,
6+
validateUnlockTime,
7+
validateRequestSize,
8+
validateKey,
9+
validateTimestamp,
10+
} from "@/lib/validation";
11+
import { ErrorCode, createErrorResponse } from "@/lib/errors";
712

813
export async function POST(request: NextRequest) {
9-
const contentLength = request.headers.get('content-length');
10-
const maxSize = parseInt(process.env.MAX_FILE_SIZE_MB || '10') * 1024 * 1024;
11-
12-
if (contentLength && parseInt(contentLength) > maxSize) {
13-
return jsonResponse({
14-
error: `Request body exceeds maximum size of ${maxSize / 1024 / 1024}MB`
15-
}, 413);
14+
const contentLength = parseInt(request.headers.get("content-length") || "0");
15+
16+
const sizeValidation = validateRequestSize(contentLength);
17+
if (!sizeValidation.valid) {
18+
return jsonResponse({ error: sizeValidation.error }, 413);
1619
}
17-
18-
return createAPIRoute(async ({ container, request: ctx, ip }) => {
19-
const formData = await ctx.formData();
20-
const encryptedBlob = formData.get('encryptedBlob') as File;
21-
const keyB = formData.get('keyB') as string;
22-
const iv = formData.get('iv') as string;
23-
const unlockTime = parseInt(formData.get('unlockTime') as string);
24-
const isDMS = formData.get('isDMS') === 'true';
25-
const pulseInterval = formData.get('pulseInterval') ?
26-
parseInt(formData.get('pulseInterval') as string) : undefined;
27-
28-
if (!encryptedBlob || !keyB || !iv || !unlockTime || isNaN(unlockTime)) {
29-
return createErrorResponse(ErrorCode.INVALID_UNLOCK_TIME, 'Missing required fields');
30-
}
31-
32-
const sizeValidation = validateFileSize(encryptedBlob.size);
33-
if (!sizeValidation.valid) {
34-
return jsonResponse({ error: sizeValidation.error }, 400);
35-
}
36-
37-
const timeValidation = validateUnlockTime(unlockTime);
38-
if (!timeValidation.valid) {
39-
return createErrorResponse(ErrorCode.INVALID_UNLOCK_TIME, timeValidation.error);
40-
}
41-
42-
const sealService = container.sealService;
43-
const blobBuffer = await encryptedBlob.arrayBuffer();
44-
const result = await sealService.createSeal({
45-
encryptedBlob: blobBuffer,
46-
keyB,
47-
iv,
48-
unlockTime,
49-
isDMS,
50-
pulseInterval,
51-
}, ip);
52-
53-
return jsonResponse({
54-
success: true,
55-
sealId: result.sealId,
56-
iv: result.iv,
57-
publicUrl: `/v/${result.sealId}`,
58-
pulseUrl: result.pulseToken ? `/pulse/${result.pulseToken}` : undefined,
59-
});
60-
}, { rateLimit: { limit: 10, window: 60000 } })(request);
20+
21+
return createAPIRoute(
22+
async ({ container, request: ctx, ip }) => {
23+
const formData = await ctx.formData();
24+
const encryptedBlob = formData.get("encryptedBlob") as File;
25+
const keyB = formData.get("keyB") as string;
26+
const iv = formData.get("iv") as string;
27+
const unlockTime = parseInt(formData.get("unlockTime") as string);
28+
const isDMS = formData.get("isDMS") === "true";
29+
const pulseInterval = formData.get("pulseInterval")
30+
? parseInt(formData.get("pulseInterval") as string)
31+
: undefined;
32+
33+
if (!encryptedBlob || !keyB || !iv || !unlockTime || isNaN(unlockTime)) {
34+
return createErrorResponse(
35+
ErrorCode.INVALID_UNLOCK_TIME,
36+
"Missing required fields",
37+
);
38+
}
39+
40+
const keyBValidation = validateKey(keyB, "Key B");
41+
if (!keyBValidation.valid) {
42+
return jsonResponse({ error: keyBValidation.error }, 400);
43+
}
44+
45+
const ivValidation = validateKey(iv, "IV");
46+
if (!ivValidation.valid) {
47+
return jsonResponse({ error: ivValidation.error }, 400);
48+
}
49+
50+
const timestampValidation = validateTimestamp(unlockTime);
51+
if (!timestampValidation.valid) {
52+
return jsonResponse({ error: timestampValidation.error }, 400);
53+
}
54+
55+
const fileSizeValidation = validateFileSize(encryptedBlob.size);
56+
if (!fileSizeValidation.valid) {
57+
return jsonResponse({ error: fileSizeValidation.error }, 400);
58+
}
59+
60+
const timeValidation = validateUnlockTime(unlockTime);
61+
if (!timeValidation.valid) {
62+
return createErrorResponse(
63+
ErrorCode.INVALID_UNLOCK_TIME,
64+
timeValidation.error,
65+
);
66+
}
67+
68+
const sealService = container.sealService;
69+
const blobBuffer = await encryptedBlob.arrayBuffer();
70+
const result = await sealService.createSeal(
71+
{
72+
encryptedBlob: blobBuffer,
73+
keyB,
74+
iv,
75+
unlockTime,
76+
isDMS,
77+
pulseInterval,
78+
},
79+
ip,
80+
);
81+
82+
return jsonResponse({
83+
success: true,
84+
sealId: result.sealId,
85+
iv: result.iv,
86+
publicUrl: `/v/${result.sealId}`,
87+
pulseUrl: result.pulseToken ? `/pulse/${result.pulseToken}` : undefined,
88+
});
89+
},
90+
{ rateLimit: { limit: 10, window: 60000 } },
91+
)(request);
6192
}

app/api/seal/[id]/route.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { NextRequest } from 'next/server';
22
import { jsonResponse } from '@/lib/apiHandler';
33
import { createAPIRoute } from '@/lib/routeHelper';
4+
import { validateSealId } from '@/lib/validation';
5+
import { isHoneypot } from '@/lib/security';
6+
import { logger } from '@/lib/logger';
47

58

69
export async function GET(
@@ -9,6 +12,21 @@ export async function GET(
912
) {
1013
return createAPIRoute(async ({ container, ip }) => {
1114
const { id: sealId } = await params;
15+
16+
const sealIdValidation = validateSealId(sealId);
17+
if (!sealIdValidation.valid) {
18+
return jsonResponse({ error: sealIdValidation.error }, 400);
19+
}
20+
21+
if (isHoneypot(sealId)) {
22+
logger.warn('honeypot_accessed', { ip, sealId, userAgent: request.headers.get('user-agent') });
23+
return jsonResponse({
24+
id: sealId,
25+
isLocked: true,
26+
unlockTime: Date.now() + 999999999999,
27+
timeRemaining: 999999999999,
28+
});
29+
}
1230
const sealService = container.sealService;
1331
const metadata = await sealService.getSeal(sealId, ip);
1432

docs/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.3.0] - 2025-12-22
11+
12+
### Added
13+
- Request size validation (30MB limit)
14+
- Seal ID format validation (32 hex characters)
15+
- Key format validation (base64, length checks)
16+
- Timestamp validation (prevent overflow)
17+
- IP address validation (IPv4/IPv6)
18+
- Honeypot seals for enumeration detection
19+
- User-agent logging in audit trail
20+
21+
### Security
22+
- Enhanced input validation across all API endpoints
23+
- Honeypot IDs: 00000000... and ffffffff...
24+
- Improved audit logging with user-agent tracking
25+
1026
## [0.2.0] - 2025-12-22
1127

1228
### Added

0 commit comments

Comments
 (0)