Skip to content

slate 0.0.32 — a traversal fix in the static file server, and Argon2id under its own name

Choose a tag to compare

@edadma edadma released this 05 Sep 23:10
· 52 commits to stable since this release

A security fix in the static file server, and Argon2id moving to the module that says which
algorithm it is.
Small beside 0.0.31, and the first item is the reason it is not waiting.

files(root) served files above the root — upgrade

Every 0.0.31-and-earlier server using files() should upgrade. safePath cut the request path
on / and percent-decoded each part afterwards, so the .. check ran on text that was not ..
yet. Both

GET /%2e%2e/secret.txt HTTP/1.1
GET /..%2fsecret.txt HTTP/1.1

passed it and then decoded into a climb: against a live serve(port, files("./public")) each
handed back a file above the root with a 200. A URL parser will not build either request — it
resolves .. and reads %2e%2e as a dotted segment before a byte reaches the network, which is why
curl needs --path-as-is — so nothing that goes through fetch could have found this, and the
test that reproduces it writes the request line onto a socket.

Every part is decoded before it is judged now, and the answer is 403 — a refusal rather than a
repair, which is the rule the file already stated — for a part that decodes to .., a decoded part
carrying /, \ or a NUL byte, and a % that is not followed by two hex digits. An empty or .
part is still dropped, and an escape meaning an ordinary character is still decoded and served:
/a%2etxt is a.txt. The joined path is then normalised and checked to be under a root normalised
once at files(), so a later mistake in the part rules is a 403 rather than a file.

A symlink is still not resolved: the check is on the name the request wrote, so a link inside the
root pointing out of it is the operator's choice, as it is in nginx. The router's :name decoding is
the same shape and is deliberately left alone — that path is cut on the separators the client
actually wrote and nothing in it is judged.

slate:password is gone; Argon2id is slate:crypto's

A breaking change, and the only one. The module was one algorithm behind four generic names, next
door to a module that spells sha256 and pbkdf2 out. It now reads as what it is:

0.0.31 0.0.32
hash(p) argon2(p)
hashStrong(p) argon2(p, { memoryCost: 65536, timeCost: 3 })
check(r, p) argon2Verify(r, p)
needsRehash(r) argon2NeedsRehash(r)
import { argon2, argon2Verify } from slate:crypto

async main()
    val stored = await argon2("correct horse")

    print(await argon2Verify(stored, "correct horse"))

The PHC bytes are unchanged, so every record already written keeps verifying. A stored
$argon2id$v=19$m=19456,t=2,p=1$… needs no migration and argon2NeedsRehash answers false about
it, exactly as needsRehash did.

hashStrong does not survive and does not need to: the parameters are a record now — memoryCost,
timeCost, parallelism, hashLength, salt, node's names, so what is written here is what
the documentation everybody already reads calls them — and the heavy profile is two numbers rather
than a second name for one pair of them. An unknown key is refused, a login thought to have been
strengthened and silently not being the worst shape that mistake takes.

And it crosses to the JavaScript back end, which is what the work was for. node carries Argon2id
in its core crypto as of node 24, and node and monocypher derive the same bytes: a record written
over a fixed salt is compared as text across the two hosts, and each verifies a record the other
made. Under an older node the two names fault with a sentence saying so. A browser has no Argon2 at
all
and crypto.subtle is not a near miss — PBKDF2 and HKDF are fast by construction — so both
refuse in a page rather than deriving something weaker. argon2NeedsRehash works everywhere, reading
the record's own parameters and deriving nothing.

slate add records the whole graph

slate.sum is now written even where the command changes no byte of the manifest. The package cache
is shared between projects, so a version somebody else had already fetched left the sum file as
incomplete as it found it, and slate deps then reported for ever that a dependency was not
recorded.

Installing

brew update
brew upgrade slate

The formula names the same nine Homebrew libraries 0.0.31 did — nothing was added to or dropped from
the link line.

The compiler floor is sysl 0.0.105.