Skip to content

Commit e175e4c

Browse files
committed
done
1 parent 1c92e46 commit e175e4c

18 files changed

Lines changed: 1093 additions & 433 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import type { Request, Response } from 'express';
3+
import { z } from 'zod';
4+
import { commonSchemas, validateAndSanitize } from '../validation.js';
5+
6+
function req(overrides: Partial<Request>): Request {
7+
return {
8+
body: {},
9+
query: {},
10+
params: {},
11+
headers: {},
12+
...overrides,
13+
} as Request;
14+
}
15+
16+
describe('validation middleware', () => {
17+
it('sanitizes and validates body, query, and params', () => {
18+
const request = req({
19+
body: { email: ' USER@EXAMPLE.COM ', note: '<script>alert(1)</script>paid' },
20+
query: { page: '2', limit: '10' },
21+
params: { id: 'payment_123' },
22+
});
23+
const next = vi.fn();
24+
25+
validateAndSanitize({
26+
body: z.object({
27+
email: commonSchemas.email,
28+
note: z.string().max(100),
29+
}),
30+
query: commonSchemas.pagination,
31+
params: z.object({ id: commonSchemas.id }),
32+
})(request, {} as Response, next);
33+
34+
expect(next).toHaveBeenCalledWith();
35+
expect(request.body.email).toBe('user@example.com');
36+
expect(request.body.note).not.toContain('<script>');
37+
expect(request.query.page).toBe(2);
38+
expect(request.params.id).toBe('payment_123');
39+
});
40+
41+
it('passes formatted validation errors to error middleware', () => {
42+
const request = req({ body: { amount: '-1' } });
43+
const next = vi.fn();
44+
45+
validateAndSanitize({
46+
body: z.object({ amount: commonSchemas.amount }),
47+
})(request, {} as Response, next);
48+
49+
expect(next).toHaveBeenCalledWith(expect.objectContaining({
50+
statusCode: 400,
51+
code: 'ERR_VALIDATION_FAILED',
52+
}));
53+
});
54+
});

backend/src/middleware/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export { slaTrackingMiddleware } from './slaTracking.js';
1616
export { traceMiddleware, TRACE_ID_HEADER } from './trace.js';
1717
export { cacheControlNoStore, CACHE_NOSTORE_HEADER, VARY_HEADER } from './cache-control.js';
1818
export { validate } from './validate.js';
19+
export {
20+
validateAndSanitize,
21+
validateRequest as validateRequestWithSanitization,
22+
validateBody,
23+
commonSchemas,
24+
type ValidationOptions,
25+
type ValidationSchemas,
26+
} from './validation.js';
1927
export { versionMiddleware } from './versioning.js';
2028
export { verifyWebhook, webhookVerifiers, rawBodyCapture, type WebhookVerificationConfig } from './webhookVerification.js';
2129
export { composeMiddleware, type MiddlewareFunction, type MiddlewareChain } from './compose.js';

backend/src/middleware/sanitize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ var InputSanitizer = /** @class */ (function () {
6767
}
6868
// XSS Protection
6969
if (options.xssProtection) {
70-
sanitized = (0, xss_1.default)(sanitized);
70+
sanitized = (typeof xss_1 === 'function' ? xss_1 : xss_1.default)(sanitized);
7171
}
7272
// HTML Sanitization
7373
if (options.htmlSanitization) {

backend/src/middleware/validate.js

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
11
"use strict";
2+
23
Object.defineProperty(exports, "__esModule", { value: true });
3-
exports.validate = void 0;
4-
var zod_1 = require("zod");
5-
/**
6-
* Reusable middleware to validate the request body against a Zod schema.
7-
* Returns a 400 Bad Request with detailed errors if validation fails.
8-
*/
9-
var validate = function (schema) {
10-
return function validateMiddleware(req, res, next) {
11-
try {
12-
schema.parse(req.body);
13-
next();
14-
}
15-
catch (error) {
16-
if (error instanceof zod_1.ZodError) {
17-
return res.status(400).json({
18-
message: 'Validation failed',
19-
errors: error.errors.map(function (err) { return ({
20-
path: err.path.join('.'),
21-
message: err.message,
22-
}); }),
23-
});
24-
}
25-
next(error);
26-
}
27-
};
4+
exports.validate = exports.validateRequest = void 0;
5+
6+
const zod_1 = require("zod");
7+
const errors_js_1 = require("../types/errors.js");
8+
9+
function formatIssues(issues) {
10+
return issues.map((err) => ({
11+
path: err.path.join(".") || "root",
12+
message: err.message,
13+
}));
14+
}
15+
16+
function isZodError(error) {
17+
return error instanceof zod_1.ZodError ||
18+
(typeof error === "object" &&
19+
error !== null &&
20+
error.name === "ZodError" &&
21+
Array.isArray(error.errors));
22+
}
23+
24+
const validateRequest = (targets) => {
25+
return function validateRequestMiddleware(req, _res, next) {
26+
try {
27+
if (targets.body) req.body = targets.body.parse(req.body ?? {});
28+
if (targets.query) req.query = targets.query.parse(req.query ?? {});
29+
if (targets.params) req.params = targets.params.parse(req.params ?? {});
30+
next();
31+
} catch (error) {
32+
if (isZodError(error)) {
33+
return next(new errors_js_1.AppError(
34+
400,
35+
"Request validation failed",
36+
"ERR_VALIDATION_FAILED",
37+
formatIssues(error.errors)
38+
));
39+
}
40+
next(error);
41+
}
42+
};
2843
};
44+
45+
exports.validateRequest = validateRequest;
46+
const validate = (schema) => (0, exports.validateRequest)({ body: schema });
2947
exports.validate = validate;
48+
exports.default = validate;

backend/src/middleware/validate.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response, NextFunction } from 'express';
22
import { ZodSchema, ZodError, ZodIssue } from 'zod';
3-
import { AppError } from './errorHandler.js';
3+
import { AppError } from '../types/errors.js';
44

55
export interface ValidationTargets {
66
body?: ZodSchema;
@@ -15,6 +15,15 @@ function formatIssues(issues: ZodIssue[]) {
1515
}));
1616
}
1717

18+
function isZodError(error: unknown): error is ZodError {
19+
return error instanceof ZodError || (
20+
typeof error === 'object' &&
21+
error !== null &&
22+
(error as { name?: string }).name === 'ZodError' &&
23+
Array.isArray((error as { errors?: unknown }).errors)
24+
);
25+
}
26+
1827
/**
1928
* Validate request body, query, and params against Zod schemas.
2029
* Returns generic 400 responses without leaking internal details.
@@ -33,7 +42,7 @@ export const validateRequest = (targets: ValidationTargets) => {
3342
}
3443
next();
3544
} catch (error) {
36-
if (error instanceof ZodError) {
45+
if (isZodError(error)) {
3746
return next(new AppError(400, 'Request validation failed', 'ERR_VALIDATION_FAILED', formatIssues(error.errors)));
3847
}
3948
next(error);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"use strict";
2+
3+
Object.defineProperty(exports, "__esModule", { value: true });
4+
exports.validateBody = exports.validateRequest = exports.validateAndSanitize = exports.commonSchemas = void 0;
5+
6+
const zod_1 = require("zod");
7+
const errors_js_1 = require("../types/errors.js");
8+
const sanitize_js_1 = require("./sanitize.js");
9+
10+
exports.commonSchemas = {
11+
uuid: zod_1.z.string().uuid(),
12+
id: zod_1.z.string().trim().min(1).max(128).regex(/^[a-zA-Z0-9._:-]+$/),
13+
email: zod_1.z.string().trim().email().max(255).transform((value) => value.toLowerCase()),
14+
pagination: zod_1.z.object({
15+
page: zod_1.z.coerce.number().int().min(1).max(1000).default(1),
16+
limit: zod_1.z.coerce.number().int().min(1).max(100).default(20),
17+
}),
18+
stellarAddress: zod_1.z.string().regex(/^G[A-Z0-9]{55}$/, "Invalid Stellar address format"),
19+
amount: zod_1.z.coerce.number().positive().finite().max(1_000_000_000),
20+
};
21+
22+
function sanitizeValue(value, options) {
23+
return sanitize_js_1.InputSanitizer.getInstance().sanitize(value, {
24+
sqlEscape: false,
25+
htmlSanitization: false,
26+
escapeHtml: false,
27+
...options,
28+
});
29+
}
30+
31+
function parseSchema(schema, value, stripUnknown) {
32+
if (stripUnknown && schema instanceof zod_1.z.ZodObject) {
33+
return schema.strip().parse(value ?? {});
34+
}
35+
return schema.parse(value ?? {});
36+
}
37+
38+
function formatZodError(error) {
39+
return error.issues.map((issue) => ({
40+
path: issue.path.join(".") || "root",
41+
message: issue.message,
42+
code: issue.code,
43+
}));
44+
}
45+
46+
function isZodError(error) {
47+
return error instanceof zod_1.ZodError ||
48+
(typeof error === "object" &&
49+
error !== null &&
50+
error.name === "ZodError" &&
51+
Array.isArray(error.errors));
52+
}
53+
54+
function validateAndSanitize(schemas, options = {}) {
55+
return function validationMiddleware(req, _res, next) {
56+
try {
57+
const shouldSanitize = options.sanitize ?? true;
58+
59+
if (schemas.body) {
60+
const value = shouldSanitize ? sanitizeValue(req.body ?? {}, options.sanitizer) : req.body;
61+
req.body = parseSchema(schemas.body, value, options.stripUnknown);
62+
}
63+
64+
if (schemas.query) {
65+
const value = shouldSanitize ? sanitizeValue(req.query ?? {}, options.sanitizer) : req.query;
66+
req.query = parseSchema(schemas.query, value, options.stripUnknown);
67+
}
68+
69+
if (schemas.params) {
70+
const value = shouldSanitize ? sanitizeValue(req.params ?? {}, options.sanitizer) : req.params;
71+
req.params = parseSchema(schemas.params, value, options.stripUnknown);
72+
}
73+
74+
if (schemas.headers) {
75+
const headers = Object.fromEntries(
76+
Object.entries(req.headers).map(([key, value]) => [key.toLowerCase(), value])
77+
);
78+
parseSchema(schemas.headers, headers, options.stripUnknown);
79+
}
80+
81+
next();
82+
} catch (error) {
83+
if (isZodError(error)) {
84+
return next(new errors_js_1.AppError(
85+
400,
86+
"Request validation failed",
87+
"ERR_VALIDATION_FAILED",
88+
formatZodError(error)
89+
));
90+
}
91+
92+
next(error);
93+
}
94+
};
95+
}
96+
97+
exports.validateAndSanitize = validateAndSanitize;
98+
exports.validateRequest = validateAndSanitize;
99+
const validateBody = (schema, options) => validateAndSanitize({ body: schema }, options);
100+
exports.validateBody = validateBody;
101+
exports.default = validateAndSanitize;

0 commit comments

Comments
 (0)