UNPKG

@safeapi/safeapi

Version:

SafeAPI: Secure, deterministic, and tamper-resistant API policy engine for Node and browser.

129 lines (128 loc) 7.46 kB
import { SafeApiPolicyError } from "./SafeApiPolicyLifecycle"; /** * Internal-only validator for SafeApiPolicy schema. * Throws SafeApiPolicyError for missing fields or duplicate rule IDs. */ export function validateSafeApiPolicySchema(policy) { if (policy.guards && !Array.isArray(policy.guards)) { throw new SafeApiPolicyError("SafeAPI policy guards must be an array", "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } if (policy.guards) { const seenGuardIds = new Set(); for (const guard of policy.guards) { if (!guard || typeof guard !== "object") { throw new SafeApiPolicyError("SafeAPI guard must be an object", "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } // Injection defense: block prototype pollution for (const key of Object.keys(guard)) { if (["__proto__", "constructor", "prototype"].includes(key)) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: prototype pollution (field: ${key}, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } } // Block non-plain objects (functions, promises, class instances, proxies) if (typeof guard === "function" || typeof guard === "symbol" || typeof guard === "bigint") { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: non-plain object (guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } if (typeof guard === "object" && guard !== null) { const proto = Object.getPrototypeOf(guard); if (proto && proto !== Object.prototype) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: non-plain object prototype (guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } } if (!guard.guardId || typeof guard.guardId !== "string") { throw new SafeApiPolicyError("SafeAPI guard missing guardId", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (seenGuardIds.has(guard.guardId)) { throw new SafeApiPolicyError(`Duplicate SafeAPI guard id: ${guard.guardId}`, "SAFEAPI_POLICY_TAMPERED", "tamper"); } seenGuardIds.add(guard.guardId); if (!Array.isArray(guard.conditions)) { throw new SafeApiPolicyError("SafeAPI guard conditions must be an array", "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } for (const cond of guard.conditions) { if (!cond || typeof cond !== "object") { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: condition not object (guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } for (const key of Object.keys(cond)) { if (["__proto__", "constructor", "prototype"].includes(key)) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: prototype pollution (field: ${key}, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } } // Block dangerous value payloads if (typeof cond.value === "function" || typeof cond.value === "symbol" || typeof cond.value === "bigint") { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: dangerous value (field: value, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } if (cond.value && typeof cond.value === "object") { const proto = Object.getPrototypeOf(cond.value); if (proto && proto !== Object.prototype && proto !== Array.prototype) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: dangerous value prototype (field: value, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } // Block non-serializable fields if (cond.value instanceof Date || cond.value instanceof RegExp || typeof cond.value === "symbol" || typeof cond.value === "function" || cond.value instanceof Map || cond.value instanceof Set) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: non-serializable value (field: value, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } // Block cyclic objects try { JSON.stringify(cond.value); } catch (e) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: cyclic value (field: value, guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } } } } } if (!policy || typeof policy !== "object") { throw new SafeApiPolicyError("SafeAPI policy must be an object", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!policy.policyVersion || typeof policy.policyVersion !== "string") { throw new SafeApiPolicyError("SafeAPI policy missing policyVersion", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!policy.ruleSetId || typeof policy.ruleSetId !== "string") { throw new SafeApiPolicyError("SafeAPI policy missing ruleSetId", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!policy.policyKind || !["auth", "rate-limit", "data-filter", "routing"].includes(policy.policyKind)) { throw new SafeApiPolicyError("SafeAPI policy missing or invalid policyKind", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!Array.isArray(policy.rules)) { throw new SafeApiPolicyError("SafeAPI policy rules must be an array", "SAFEAPI_POLICY_TAMPERED", "tamper"); } const seenIds = new Set(); for (const rule of policy.rules) { if (!rule || typeof rule !== "object") { throw new SafeApiPolicyError("SafeAPI policy rule must be an object", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!rule.ruleId || typeof rule.ruleId !== "string") { throw new SafeApiPolicyError("SafeAPI policy rule missing ruleId", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (seenIds.has(rule.ruleId)) { throw new SafeApiPolicyError(`Duplicate SafeAPI rule id: ${rule.ruleId}`, "SAFEAPI_POLICY_TAMPERED", "tamper"); } seenIds.add(rule.ruleId); } } /** * Internal-only normalizer for SafeApiPolicy schema. * Returns a normalized, sorted, and deeply frozen policy object. */ export function normalizeSafeApiPolicySchema(policy) { validateSafeApiPolicySchema(policy); const normalizedRules = [...policy.rules] .map((rule) => ({ ...rule })) .sort((a, b) => a.ruleId.localeCompare(b.ruleId)); let normalizedGuards = undefined; if (policy.guards) { normalizedGuards = [...policy.guards] .map((guard) => ({ ...guard, conditions: [...guard.conditions], })) .sort((a, b) => a.guardId.localeCompare(b.guardId)); } return Object.freeze({ ...policy, rules: normalizedRules, guards: normalizedGuards, }); }