Security
rut.ts is built to sit on the path where untrusted input becomes a trusted identity. Version 4 exists specifically to make that path safe. This page explains the threat model and how to use the library defensively.
Found a vulnerability? Please report it privately via the repository's security policy rather than a public issue.
Threat model#
A RUT validator typically runs on attacker-controlled input — form fields, API bodies, query strings. That exposes three concrete risks:
- Algorithmic complexity (ReDoS): a naive regex can be driven into catastrophic backtracking by a crafted string, stalling the event loop.
- Data leakage: echoing the offending value into an error message leaks a Chilean national ID into logs, traces, and error trackers.
- Weak acceptance: treating “looks like a RUT” as “is a valid RUT” lets placeholders and malformed values through.
rut.ts addresses each one directly.
How rut.ts is hardened#
Bounded input parsing#
Every entry point caps input length before any parsing or regex runs, so oversized strings fail fast instead of spending time in the matcher. This neutralizes ReDoS-style abuse.
import { validate } from 'rut.ts'
// A megabyte of digits doesn't run the matcher — it's rejected up front.
validate('9'.repeat(1_000_000)) // false, immediatelyStrict, canonical validation#
validate() accepts only canonical RUT shapes and rejects ambiguous dot grouping. strict: true additionally rejects repeated-digit placeholders that are technically valid but never real.
import { validate } from 'rut.ts'
validate('12.345.678-5') // true
validate('12.345678-5') // false (ambiguous grouping)
validate('11.111.111-1') // true (valid check digit)
validate('11.111.111-1', { strict: true }) // false (placeholder)Use validate(input, { strict: true }) as the final acceptance gate
for real identities. clean(), decompose(), getBody(), and
getVerifier() are permissive normalizers — a non-null result does
not mean the verifier digit is correct.
Generic error messages#
Errors are intentionally generic (Invalid RUT input) and never include the offending value, so national ID numbers cannot leak through exception messages into your observability stack.
import { clean } from 'rut.ts'
try {
clean('invalid')
} catch (e) {
console.error((e as Error).message) // "Invalid RUT input" — no value echoed
}Crypto-backed generation#
generate() uses the Web Crypto API when available, produces non-suspicious bodies, and every result passes validate(rut, { strict: true }).
import { generate, validate } from 'rut.ts'
validate(generate(), { strict: true }) // always trueGenerated RUTs are for tests, demos, and seed data only. Never assign a generated value as a real person's identity.
A safe-by-default acceptance pipeline#
The recommended shape for any endpoint that accepts a RUT from the outside world:
import { validate, clean } from 'rut.ts'
export function acceptRut(input: unknown):
| { ok: true; rut: string }
| { ok: false } {
// 1. Type guard — non-strings are invalid, no TypeError thrown.
if (typeof input !== 'string') return { ok: false }
// 2. Strict acceptance gate: bounded, canonical, verifier-checked,
// no placeholders.
if (!validate(input, { strict: true })) return { ok: false }
// 3. Normalize to one canonical stored shape.
return { ok: true, rut: clean(input) }
}Defensive checklist#
Validate strictly before you trust
validate(input, { strict: true }) is the gate. Everything else is
normalization or display.
Use safe mode at boundaries
Pass { throwOnError: false } to clean/format/decompose on
untrusted input and branch on null.
Never log raw input
Keep error messages generic; do not add the offending value back into your own logs around the call site.
Store one canonical shape
clean() (or decompose()) after validation so comparisons and
lookups are consistent.
Maintenance#
rut.ts is actively maintained: a 394-case test suite (12 suites, 100% pass) plus a differential harness that diffs behavior against real-world inputs, with ongoing edge-case and benchmark work. See Testing for how coverage is structured.