fortify-schema
Version:
A modern TypeScript validation library designed around familiar interface syntax and powerful conditional validation. Experience schema validation that feels natural to TypeScript developers while unlocking advanced runtime validation capabilities.
367 lines (325 loc) • 9.69 kB
Plain Text
import Ajv from "ajv";
import addFormats from "ajv-formats";
export class SecurityValidators {
private ajv: Ajv;
private secureAjv: Ajv;
constructor() {
this._initializeFastValidator();
this._initializeSecureValidator();
this._addSecurityKeywords();
}
/**
* Initialize fast validator with minimal security for performance
*/
private _initializeFastValidator(): void {
this.ajv = new Ajv({
allErrors: true,
verbose: true,
strict: false,
validateFormats: false,
});
addFormats(this.ajv);
}
/**
* Initialize secure validator with maximum security features
*/
private _initializeSecureValidator(): void {
this.secureAjv = new Ajv({
allErrors: true,
verbose: true,
strict: true,
validateFormats: true,
removeAdditional: "all",
useDefaults: false,
coerceTypes: false,
addUsedSchema: false,
inlineRefs: false,
loadSchema: false,
code: {
optimize: false,
es5: true,
},
});
addFormats(this.secureAjv, { mode: "full" });
}
/**
* Add custom security keywords to prevent common attacks
*/
private _addSecurityKeywords(): void {
this._addPrototypePollutionKeyword();
this._addConstructorManipulationKeyword();
this._addDeepPropertyKeyword();
}
/**
* Add keyword to prevent prototype pollution attacks
*/
private _addPrototypePollutionKeyword(): void {
this.secureAjv.addKeyword({
keyword: "noPrototypePollution",
type: "object",
compile: () => {
return (data: any) => {
if (!this._isObject(data)) return true;
return this._validatePrototypePollution(data);
};
},
});
}
/**
* Add keyword to prevent constructor manipulation
*/
private _addConstructorManipulationKeyword(): void {
this.secureAjv.addKeyword({
keyword: "noConstructorManipulation",
type: "object",
compile: () => {
return (data: any) => {
if (!this._isObject(data)) return true;
return this._validateConstructorManipulation(data);
};
},
});
}
/**
* Add keyword for deep property validation
*/
private _addDeepPropertyKeyword(): void {
this.secureAjv.addKeyword({
keyword: "deepSecurityCheck",
type: "object",
compile: () => {
return (data: any) => {
if (!this._isObject(data)) return true;
return this._performDeepSecurityCheck(data);
};
},
});
}
/**
* Validate against prototype pollution attacks
*/
private _validatePrototypePollution(data: any, path = ""): boolean {
const dangerousProps = this._getDangerousProperties();
for (const key of Object.keys(data)) {
if (dangerousProps.includes(key)) {
this._setValidationError(
"noPrototypePollution",
path + "/" + key,
`Dangerous property "${key}" detected - potential prototype pollution`
);
return false;
}
if (this._isObject(data[key])) {
if (!this._validatePrototypePollution(data[key], path + "/" + key)) {
return false;
}
}
}
return true;
}
/**
* Validate against constructor manipulation
*/
private _validateConstructorManipulation(data: any): boolean {
if (
"constructor" in data &&
this._isConstructorManipulation(data.constructor)
) {
this._setValidationError(
"noConstructorManipulation",
"/constructor",
"Constructor property manipulation detected"
);
return false;
}
return true;
}
/**
* Perform deep security check on nested objects
*/
private _performDeepSecurityCheck(data: any, depth = 0): boolean {
if (depth > this._getMaxDepth()) {
this._setValidationError(
"deepSecurityCheck",
"",
"Maximum object depth exceeded"
);
return false;
}
if (!this._isObject(data)) return true;
for (const [key, value] of Object.entries(data)) {
if (!this._isPropertyNameSafe(key)) {
this._setValidationError(
"deepSecurityCheck",
"/" + key,
`Unsafe property name: ${key}`
);
return false;
}
if (this._isObject(value)) {
if (!this._performDeepSecurityCheck(value, depth + 1)) {
return false;
}
}
}
return true;
}
/**
* Get list of dangerous property names
*/
private _getDangerousProperties(): string[] {
return [
"__proto__",
"constructor",
"prototype",
"__defineGetter__",
"__defineSetter__",
];
}
/**
* Check if value is a plain object
*/
private _isObject(value: any): boolean {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Check if constructor property indicates manipulation
*/
private _isConstructorManipulation(constructor: any): boolean {
return (
this._isObject(constructor) &&
("prototype" in constructor || "constructor" in constructor)
);
}
/**
* Check if property name is safe
*/
private _isPropertyNameSafe(propName: string): boolean {
const unsafePatterns = [
/^__/, // Double underscore prefix
/prototype$/, // Ends with prototype
/constructor$/, // Ends with constructor
/^eval$/, // Eval function
/^Function$/, // Function constructor
];
return !unsafePatterns.some((pattern) => pattern.test(propName));
}
/**
* Get maximum allowed object depth
*/
private _getMaxDepth(): number {
return 50; // Configurable depth limit
}
/**
* Set validation error for current validation context
*/
private _setValidationError(
keyword: string,
path: string,
message: string
): void {
// This would be set on the current validation function context
// Implementation depends on AJV's internal error handling
}
/**
* Create enhanced secure schema with security keywords
*/
private _createSecureSchema(baseSchema: object): object {
return {
...baseSchema,
noPrototypePollution: true,
noConstructorManipulation: true,
deepSecurityCheck: true,
additionalProperties: false,
};
}
/**
* Sanitize data before validation
*/
private _sanitizeData(data: any): any {
if (!this._isObject(data)) return data;
const sanitized = {};
const dangerousProps = this._getDangerousProperties();
for (const [key, value] of Object.entries(data)) {
if (!dangerousProps.includes(key) && this._isPropertyNameSafe(key)) {
sanitized[key] = this._isObject(value)
? this._sanitizeData(value)
: value;
}
}
return sanitized;
}
/**
* Compile validator with caching
*/
private _compileValidator(ajvInstance: Ajv, schema: object): any {
// In production, you might want to cache compiled validators
return ajvInstance.compile(schema);
}
/**
* Format validation errors for better debugging
*/
private _formatErrors(errors: any[]): any[] {
return errors.map((error) => ({
...error,
severity: this._getErrorSeverity(error.keyword),
suggestion: this._getErrorSuggestion(error.keyword),
}));
}
/**
* Get error severity based on keyword
*/
private _getErrorSeverity(
keyword: string
): "low" | "medium" | "high" | "critical" {
const severityMap = {
noPrototypePollution: "critical",
noConstructorManipulation: "critical",
deepSecurityCheck: "high",
additionalProperties: "medium",
type: "medium",
required: "low",
};
return severityMap[keyword] || "low";
}
/**
* Get suggestion for fixing validation error
*/
private _getErrorSuggestion(keyword: string): string {
const suggestions = {
noPrototypePollution:
"Remove dangerous properties like __proto__, constructor, prototype",
noConstructorManipulation: "Avoid manipulating constructor properties",
deepSecurityCheck:
"Check for unsafe property names and excessive nesting",
additionalProperties: "Remove unexpected properties from object",
type: "Ensure property has correct data type",
required: "Add missing required properties",
};
return suggestions[keyword] || "Check schema requirements";
}
// Public API - keeping your original method names
validateFast(data: any, schema: object): { valid: boolean; errors?: any[] } {
const validate = this._compileValidator(this.ajv, schema);
const valid = validate(data);
return {
valid,
errors: validate.errors ? this._formatErrors(validate.errors) : undefined,
};
}
validateSecure(
data: any,
schema: object
): { valid: boolean; errors?: any[] } {
// Sanitize data first
const sanitizedData = this._sanitizeData(data);
// Create secure schema with security keywords
const secureSchema = this._createSecureSchema(schema);
const validate = this._compileValidator(this.secureAjv, secureSchema);
const valid = validate(sanitizedData);
return {
valid,
errors: validate.errors ? this._formatErrors(validate.errors) : undefined,
};
}
}