@llui/security

The tiny, dependency-free home of LLui's security-sensitive primitives, owned in exactly one place so a fix to any of them can't drift between copies. Everything here uses only the WHATWG URL API plus regex — no node:* builtins — so it is safe to import from browser bundles.

Two surfaces:

pnpm add @llui/security

Usage

import { sanitizeUrl, defaultAllowedProtocols } from '@llui/security/url'
import { isLoopbackOrigin, isLoopbackAuthority } from '@llui/security/loopback'

// Neutralize a javascript:/data: URL before it reaches an href/src sink.
const href = sanitizeUrl(userSuppliedUrl) // null when the scheme is not allowed

// Gate a dev-server request to same-machine callers.
if (!isLoopbackOrigin(req.headers.origin)) reject()

Entry points

Import Purpose
@llui/security Barrel — re-exports both modules below
@llui/security/url sanitizeUrl + defaultAllowedProtocols scheme allow-listing
@llui/security/loopback isLoopbackHost / isLoopbackAuthority / isLoopbackOrigin recognizers

Functions

isLoopbackAuthority()

True when an authority (host or host:port, IPv6 bracketed as [::1]:port) is a loopback host. An ABSENT/empty authority → false: a request with no Host header is not provably same-machine, so it must not pass the guard.

function isLoopbackAuthority(authority: string | undefined): boolean

isLoopbackHost()

True when host — a bare hostname with NO port (IPv6 may be bracketed [::1] or bare ::1) — names the loopback interface.

function isLoopbackHost(host: string): boolean

isLoopbackOrigin()

True when an Origin header value is same-origin/local: either ABSENT (a native, non-browser client sends none) or a loopback host. A cross-origin browser page (CSWSH / drive-by hijack) presents a non-loopback Origin and is rejected. A literal Origin: null (sandboxed / file: / data: context) fails new URL and is likewise rejected — it is NOT the same as an absent header. IPv6 loopback origins arrive bracketed (http://[::1]), and WHATWG URL.hostname keeps the brackets ([::1]); {@link isLoopbackHost} strips them before the comparison so bracketed IPv6 loopback is recognised.

function isLoopbackOrigin(origin: string | undefined): boolean

sanitizeUrl()

Returns the URL unchanged if its scheme is on allowedProtocols (or it is a relative/anchor/query URL — always safe), otherwise null. Mirrors micromark's sanitizeUri: a scheme only "counts" when its colon precedes any /, ?, or #. Tab/CR/LF are stripped and leading control/space chars ignored first, the way a browser does — so java\tscript: or a leading control char cannot hide a dangerous scheme. allowedProtocols defaults to {@link defaultAllowedProtocols}.

function sanitizeUrl(url: string, allowedProtocols: readonly string[] = defaultAllowedProtocols): string | null

Constants

defaultAllowedProtocols

The schemes permitted by default in links (and, via markdown, images). Relative URLs (no scheme) are always allowed regardless of this list. This is the shared baseline every consumer builds on instead of hand-rolling a divergent allowlist.

const defaultAllowedProtocols: readonly string[]