According to the KoaJS description, this is an expressive HTTP middleware framework for Node.js to make web applications and APIs more enjoyable to write. It has ~30k GitHub stars, is downloaded 200k times per week, and is a dependency of almost 4k npm packages.
We can assume that a lot of public websites are using it.
Session cookie vulnerable by default
In every GitHub example, they specify app.keys as a list of strings. If you are doing it, you are vulnerable to an offline known-plaintext attack.
koajs/koa uses the library pillarjs/cookies, which uses crypto-utils/keygrip to sign and verify the session cookie.
In the koajs/koa package, a new Cookies is created if none exists:
The Cookies function takes options as a third argument, with a keys parameter equal to the app.keys value in the configuration.
In the index.js of pillarjs/cookies:
In the Cookies function, only the else condition matches the koajs/koa options. If options.keys is an array, it returns a new Keygrip object with the default hash algorithm and encoding — SHA-1 and base64. Otherwise it returns options.keys as-is.
To sign the session cookie, Keygrip performs an HMAC(algorithm, keys) of the session cookie value, encodes it and replaces some characters.
Using HMAC is secure in production — but only when configured properly. Since SHA-1 is broken, you must be careful about how HMAC is implemented. Two options: set app.keys to an array of 20-byte strings, or use a stronger hash function (SHA-2).
To override the hash algorithm, set options.keys as a Keygrip constructor that redefines the hash function and encoding:
The KoaJS team was informed of this issue and pull requests to patch the README have been merged. This comment summarizes the issue well:
tl;dr the content of the cookie is not encoded, only the .sig when signed is used. Also, by default it uses the sha1 algorithm to generate the .sig cookie, which can be cracked in minutes.
The keys must meet the minimum length requirements for HMAC:
| Hash algorithm | Minimum key length |
|---|---|
| SHA-1 | 20 bytes |
| SHA-256 | 32 bytes |
| SHA-512 | 64 bytes |