UNPKG

@safeapi/safeapi

Version:

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

131 lines (130 loc) 7.17 kB
import { SafeApiPolicyError } from "./SafeApiPolicyLifecycle"; function compare(operator, left, right) { switch (operator) { case "equals": return left === right; case "not_equals": return left !== right; case "gt": return typeof left === "number" && typeof right === "number" && left > right; case "lt": return typeof left === "number" && typeof right === "number" && left < right; case "contains": return typeof left === "string" && typeof right === "string" && left.includes(right); case "in": return Array.isArray(left) && left.includes(right); default: return false; } } function getFieldValue(context, field) { // Only support top-level fields and metadata for now if (field in context) return context[field]; if (context.metadata && field in context.metadata) return context.metadata[field]; return undefined; } /** * Deterministic guard evaluation engine for SafeAPI. * @internal */ export function evaluateGuards(guards = [], requestContext, options) { // Tamper defense: check deep-frozen guards if (!Object.isFrozen(guards)) { throw new SafeApiPolicyError("SAFEAPI_POLICY_TAMPERED: guards not frozen", "SAFEAPI_POLICY_TAMPERED", "tamper"); } if (!guards || guards.length === 0) { return Object.freeze({ allowed: true, guardTrace: options?.trace ? Object.freeze([]) : undefined }); } // Sort guards by guardId for determinism const sorted = [...guards].sort((a, b) => a.guardId.localeCompare(b.guardId)); const matchingGuards = []; const traces = []; let logicalClock = 0; for (const guard of sorted) { // Tamper defense: check deep-frozen guard if (!Object.isFrozen(guard)) { throw new SafeApiPolicyError(`SAFEAPI_POLICY_TAMPERED: guard not frozen (guardId: ${guard.guardId})`, "SAFEAPI_POLICY_TAMPERED", "tamper"); } // Injection defense: block prototype pollution and dangerous objects 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"); } const value = guard[key]; if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: non-plain object (guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } if (value && typeof value === "object") { const proto = Object.getPrototypeOf(value); if (proto && proto !== Object.prototype && proto !== Array.prototype) { throw new SafeApiPolicyError(`SAFEAPI_GUARD_INJECTION_BLOCKED: non-plain object prototype (guardId: ${guard.guardId})`, "SAFEAPI_GUARD_INJECTION_BLOCKED", "injection"); } } } // Tamper defense: check deep-frozen conditions if (!Object.isFrozen(guard.conditions)) { throw new SafeApiPolicyError(`SAFEAPI_POLICY_TAMPERED: conditions not frozen (guardId: ${guard.guardId})`, "SAFEAPI_POLICY_TAMPERED", "tamper"); } const evaluatedConditions = guard.conditions.map(cond => { // Tamper defense: check deep-frozen condition if (!Object.isFrozen(cond)) { throw new SafeApiPolicyError(`SAFEAPI_POLICY_TAMPERED: condition not frozen (guardId: ${guard.guardId})`, "SAFEAPI_POLICY_TAMPERED", "tamper"); } 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"); } } 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"); } 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"); } 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"); } } const left = getFieldValue(requestContext, cond.field); return Object.freeze({ field: cond.field, operator: cond.operator, value: cond.value, matched: compare(cond.operator, left, cond.value), }); }); const allMatch = evaluatedConditions.every(c => c.matched); traces.push(Object.freeze({ guardId: guard.guardId, matched: allMatch, effect: guard.effect, evaluatedConditions: Object.freeze(evaluatedConditions), evaluatedAt: `logical-${++logicalClock}`, })); if (allMatch) { matchingGuards.push(guard); } } // Deny always wins if any matching deny guard exists const denyGuard = matchingGuards.find(g => g.effect === "deny"); if (denyGuard) { return Object.freeze({ allowed: false, matchedGuard: denyGuard.guardId, guardTrace: options?.trace ? Object.freeze(traces) : undefined }); } // Otherwise, allow wins if any matching allow guard exists const allowGuard = matchingGuards.find(g => g.effect === "allow"); if (allowGuard) { return Object.freeze({ allowed: true, matchedGuard: allowGuard.guardId, guardTrace: options?.trace ? Object.freeze(traces) : undefined }); } // No matches, allow by default return Object.freeze({ allowed: true, guardTrace: options?.trace ? Object.freeze(traces) : undefined }); }