Zornux docs
Get started Spec

Language

Security

Security in Zornux is a language-level capability, not a web add-on. Trust rules and authorization are enforced by the runtime itself, so every program — not just controllers — gets the same guarantees.

Three built-in defenses

ConcernZornux mechanism
Authenticationsecure my_app using SimpleAuthentication
Authorizationrestrict to Admin otherwise give back status 403
Data safetyUntrustedText isolation — external input can't be combined with trusted text until it's sanitized or validated.

Authorization

A restrict to <Role> guard runs before the block it protects. The runtime checks the current security context; if the role (or permission) is missing, the otherwise branch decides what happens — and the protected code never runs.

zornux
controller Products at "/products"
    require role "admin"

    on POST "/" with request_data
        add request_data to products
        give back created message "Product created!"
    end
end
Where the context comes from

The security context (the current identity, its roles and permissions) is part of the runtime. For controllers, the host supplies it per request; the language itself — never the web layer — enforces the restrict check.

Untrusted input is its own type

Data arriving from outside your program is UntrustedText. You can show it, but you cannot combine it with trusted Text using + — the classic injection vector — until you explicitly clean it. Crossing that line raises ZX1205 at runtime, turning "remember to sanitize" into a rule the language enforces.

zornux
create comment = untrusted(read_text("comment.txt"))

# show "New comment: " + comment   # error ZX1205 — untrusted + trusted

create safe = sanitize(comment)     # strips dangerous characters → Text
show "New comment: " + safe         # fine — 'safe' is trusted now

The trust-aware built-ins

Built-inWhat it does
untrusted(text)Marks a value as UntrustedText.
sanitize(text)Removes dangerous characters and returns trusted Text.
sanitize_lines(text)Like sanitize, but keeps newlines and tabs — for multi-line untrusted input (file bodies, request bodies) that must keep its structure.
validate(text)Returns a Truth — whether the value is already clean.
trust(text)Advanced override: trusts a value without cleaning it. Use sparingly.
read_text returns UntrustedText

File contents come from outside the program, so read_text hands you UntrustedText by design — you decide when it becomes trusted. Note that text(...) converts kinds but preserves taint — it is not a laundering path; go through sanitize / sanitize_lines / trust.

Authentication providers

Attach an authentication provider declaratively with secure … using. The provider name must be registered, or you get ZX1206. Zornux ships an in-memory SimpleAuthentication provider; the abstraction is shaped so real providers (passkeys, API keys, LDAP) slot in without changing your program.

zornux
secure my_app using SimpleAuthentication
OAuth 2.0 / OIDC, built in

For real-world sign-in, Zornux is a first-class OpenID Connect relying party. The auth module runs the Authorization Code flow with mandatory PKCE (auth.discover, auth.begin_login, auth.complete_login, auth.refresh_login, auth.logout_url, auth.verify_jwt) and verifies tokens against the provider's JWKS (RS256/ES256; alg: none refused, exp required). MFA can be delegated to that provider — or handled in-process with native TOTP (see below).

Native authentication toolkit

Beyond delegated sign-in, the auth module carries the pieces of an authentication system you'd otherwise bolt on — in-process, deterministic, and constant-time where it matters.

TOTP multi-factor

auth.totp_secret mints a Base32 secret, auth.totp_uri builds the provisioning URI an authenticator app scans, auth.totp_code produces the current code, and auth.verify_totp checks a submitted one — RFC 6238, with a small clock-drift window.

zornux
import auth

# Enrollment — a per-user secret and a URI the authenticator app scans.
public function enroll
    create secret = auth.totp_secret()
    create uri = auth.totp_uri(secret, "ada@example.com", "Zornux")
    give back secret
end

# Login step two — verify the 6-digit code.
public function verify_second_factor with secret, submitted_code
    give back auth.verify_totp(secret, submitted_code)
end

Sessions, bearer tokens, and refresh

auth.sign_in establishes the principal. In a serve app a JWT or session cookie is verified (HS256, constant-time) and bridged into the request's security context via the auth_bearer_secret setting, so restrict, policies, and claim(...) see the real caller. Refresh-token rotation keeps the rotated-away token as a tombstone and revokes the entire session family if a superseded token is ever replayed — the OWASP reuse defence, on by default.

Built-inPurpose
auth.has_role / has_permission / has_scope / canRole, permission, OAuth-scope, and ability checks.
auth.deny(request)Correct denial status — 401 when anonymous, 403 when authenticated but unauthorized.
auth.generate_api_key / verify_api_key / fingerprint_api_keyIssue and constant-time-verify API keys.
auth.lockout / record_failure / record_success / is_lockedBrute-force lockout after repeated failures.
auth.create_token / read_token / verify_jwtMint and verify signed tokens.
Default-deny, always

Authorization guards fail closed: an unauthenticated caller, a crashed check, or a non-truth result all deny. Use auth.deny(request) so the status code is correct too — a 401 for "who are you?", a 403 for "not allowed."

Policies & claims

For authorization that's more than a single role, Zornux has first-class policies: a policy … end block names a set of require rules (authentication, roles, permissions, claims, even other policies), and resource-based check blocks decide with ordinary code. They're enforced with the same guard — restrict to policy Name otherwise … — and they fail closed.

zornux
policy CanManageOrders
    require authentication
    require role "Manager" or "Owner"
end

restrict to policy CanManageOrders otherwise give back status 403

See the dedicated Authorization page for claims, custom check blocks with the injected user, resource-based policies, and automatic denial logging.

Memory safety

Zornux is memory-safe by design. There are no pointers, no manual memory to allocate or free, and no way to reach an address or read past the end of a list — so whole classes of bugs simply can't be written. Values are managed for you, and when a program pushes a real limit it stops with a clear, located message instead of crashing.

ProtectionWhat it means for you
Bounds-checked accessReading a list past its end is a friendly error, not a bad read; an unknown map key reads as nothing.
Depth-bounded recursionA function that calls itself forever is stopped safely — and you can catch it with try … catch error.
Bounded growthA host can cap how large a list, map, or piece of text may grow, and how deeply data may nest; turning a value that contains itself into JSON stops rather than looping forever.
Sandboxed files & packagesFile access stays inside an allowed folder (links can't escape it), and packages are checksum-verified and unpacked with protection against path-escape and decompression-bomb archives.
zornux
function descend with n
    give back descend(n + 1)     # runaway recursion
end

try
    show descend(1)
catch error
    show "stopped safely"        # the program keeps control
end
A limit is a message, not a crash

When a program reaches a memory-safety limit — depth, size, or a malformed package — it stops with a located diagnostic you can read and recover from, exactly the same way every time.

Checking for weaknesses

Beyond the guarantees above, zornux check --security reads your program the way a reviewer would and reports what it finds: a secret written into the source, untrusted data reaching a query or a command, a resource opened and never closed, a route that changes data with nothing guarding it, a cookie giving up a safe default. Each finding explains itself and suggests a fix. It's opt-in, so an ordinary check is unchanged.

Auditing your dependencies

Point it at an advisory feed and it will also tell you when a package you depend on has a known vulnerability. The feed is a file you supply — the compiler never reaches the network to fetch one.

bash
zornux check app.zx --security --advisories advisories.json

Advisories are matched against the versions your project actually resolved, including packages you depend on only indirectly. Your project is found by looking upward from the file you checked, so the same versions are read wherever your program lives.

The audit fails closed, because an audit that quietly skips something is worse than no audit at all. A dependency whose version isn't resolved yet is reported as not audited — never as safe; run zornux restore first, and it will be checked like the rest. An advisory written in a way the check cannot read is named in the output and stops the check, rather than passing the package it was meant to catch. An advisory whose severity is unrecognized is treated as an error, never quietly downgraded.

Silence is never a pass

Everything the check could not establish is reported in its output, not only to the person reading the screen — so an automated build, which sees nothing but a pass or a fail, sees the same truth you do.

Secure-by-default, in two rules

1. External input is UntrustedText until sanitized or validated. 2. Authorization guards run before the blocks they protect.

For the formal rules behind these guarantees, see the Specification and the Diagnostics catalog (ZX1200–1299).