Initial Commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Safety Checks
|
||||
|
||||
## Purpose
|
||||
|
||||
Production-time defensive checks against pathological inputs to the bitpacker — schemas with infinite recursion, arrays with millions of elements, objects with thousands of keys, floats that are `NaN` or `Infinity`. The `SafetyChecker` class wraps every public-boundary pack/unpack call in a lightweight check that fails fast on the limits it was constructed with, before the bitpacker hot path consumes excessive memory or compute. Safety is opt-in (each `SafetyEnabledSchema` carries an embedded `_safety` block) and selective (the `SafetyPerformanceOptions` lets a consumer disable individual checks for benchmarked-acceptable workloads).
|
||||
|
||||
The module also ships the `RecursionGuard` factory that the `BitPacker.unpack` path uses to enforce `maxRecursionDepth`. Without the guard, an attacker-controlled schema with a self-referential `_ref` could blow the JS stack via O(n) recursion on a large input. The guard's `withGuard(fn)` form auto-cleans up on exception so a deeply-nested unpack failure doesn't leave the depth counter wedged.
|
||||
|
||||
## Features
|
||||
|
||||
- `SafetyChecker` — main class, constructed with `SafetyLimits` + optional `SafetyPerformanceOptions`
|
||||
- `createRecursionGuard(checker)` — factory for a `RecursionGuard` with `enter`/`exit`/`withGuard`
|
||||
- `validatePackedSize(bytes, checker)` / `estimatePackedSize(schema, data)` — pre-flight size checks
|
||||
- `SafetyError` — thrown on any limit breach, with a stable `code` field
|
||||
- Six published codes: `BUFFER_OVERFLOW`, `RECURSION_OVERFLOW`, `ARRAY_TOO_LARGE`, `OBJECT_TOO_LARGE`, `INVALID_FLOAT`, `CUSTOM_VALIDATION_FAILED`
|
||||
- `DEFAULT_SAFETY_LIMITS` — `{ maxBufferSize: 10MB, maxRecursionDepth: 100, maxArrayLength: 10_000, maxObjectKeys: 1_000, allowInfiniteFloats: false, allowNaNFloats: false }`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SafetyChecker, DEFAULT_SAFETY_LIMITS } from "@suaveplan/bitpacker-validation";
|
||||
|
||||
const checker = new SafetyChecker(DEFAULT_SAFETY_LIMITS);
|
||||
checker.checkArray(new Array(5000)); // ok
|
||||
checker.checkArray(new Array(50_000)); // throws SafetyError(ARRAY_TOO_LARGE)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SafetyChecker,
|
||||
SafetyError,
|
||||
createRecursionGuard,
|
||||
} from "@suaveplan/bitpacker-validation";
|
||||
|
||||
const checker = new SafetyChecker({
|
||||
maxBufferSize: 1024 * 1024,
|
||||
maxRecursionDepth: 32,
|
||||
maxArrayLength: 1000,
|
||||
maxObjectKeys: 100,
|
||||
allowInfiniteFloats: false,
|
||||
allowNaNFloats: false,
|
||||
});
|
||||
|
||||
const guard = createRecursionGuard(checker);
|
||||
|
||||
function unpackSafely(schema: unknown, data: unknown): unknown {
|
||||
return guard.withGuard(() => {
|
||||
try {
|
||||
return performUnpack(schema, data);
|
||||
} catch (err) {
|
||||
if (err instanceof SafetyError && err.code === "RECURSION_OVERFLOW") {
|
||||
// …emit metric, fall back to a depth-limited representation…
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SafetyChecker`
|
||||
|
||||
Constructor: `new SafetyChecker(limits?, performance?)`. `limits` defaults to `DEFAULT_SAFETY_LIMITS`. `performance` defaults to `{}` (every check enabled).
|
||||
|
||||
#### `checkArray(arr: unknown[])`, `checkObject(obj: object)`, `checkBuffer(bytes: Uint8Array)`, `checkFloat(value: number)`
|
||||
|
||||
Each method throws `SafetyError` with the corresponding code on limit breach.
|
||||
|
||||
#### `getMetrics(): SafetyMetrics`
|
||||
|
||||
Returns the per-violation counters and the timestamp of the last violation.
|
||||
|
||||
### `function createRecursionGuard(checker: SafetyChecker): RecursionGuard`
|
||||
|
||||
Returns a guard with `enter()`, `exit()`, and `withGuard(fn)`. `withGuard` auto-cleans on exception.
|
||||
|
||||
### `class SafetyError extends Error`
|
||||
|
||||
`code: string`. Six published codes (see Features).
|
||||
|
||||
### `function estimatePackedSize(schema, data)` / `validatePackedSize(bytes, checker)`
|
||||
|
||||
Pre-flight estimation and post-pack validation. The estimator is cheap; use it before `checker.checkBuffer` to short-circuit clearly-oversize packs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Custom validators run after built-ins.** A `SchemaSafetyLimits.customValidators` entry resolves to a registered `CustomSafetyValidator` and runs after the built-in checks pass. A custom validator may throw `SafetyError(CUSTOM_VALIDATION_FAILED)` to surface its own constraint name.
|
||||
- **`RecursionGuard.withGuard` is exception-safe.** The implementation uses `try { … } finally { exit() }` so the depth counter always decrements, even if the wrapped function throws. Calling `enter` without a matching `exit` would leak depth and cause spurious `RECURSION_OVERFLOW` later.
|
||||
- **`checkFloat` is opt-in via `allowInfiniteFloats` / `allowNaNFloats`.** Both default to `false` because most game and protocol schemas have no legitimate use for `Infinity` or `NaN`. Consumers that legitimately need them set the flags to `true` and accept the risk.
|
||||
- **Metrics are aggregate, not per-call.** `SafetyMetrics` accumulates violation counts across the checker's lifetime. Callers who want per-call metrics must reset the checker between calls or accumulate at their own layer.
|
||||
Reference in New Issue
Block a user