@safeapi/safeapi
Version:
SafeAPI: Secure, deterministic, and tamper-resistant API policy engine for Node and browser.
129 lines (128 loc) • 7.46 kB
JavaScript
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,
});
}