Skip to content

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:

  1. Algorithmic complexity (ReDoS): a naive regex can be driven into catastrophic backtracking by a crafted string, stalling the event loop.
  2. Data leakage: echoing the offending value into an error message leaks a Chilean national ID into logs, traces, and error trackers.
  3. 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.

TypeScript
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, immediately

Strict, 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.

TypeScript
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.

TypeScript
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 }).

TypeScript
import { generate, validate } from 'rut.ts'
 
validate(generate(), { strict: true }) // always true

Generated 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:

TypeScript
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#

1

Validate strictly before you trust

validate(input, { strict: true }) is the gate. Everything else is normalization or display.

2

Use safe mode at boundaries

Pass { throwOnError: false } to clean/format/decompose on untrusted input and branch on null.

3

Never log raw input

Keep error messages generic; do not add the offending value back into your own logs around the call site.

4

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.