fix: harden the search token file permissions and hash digest - #2223
Merged
Conversation
The token is the CSRF/auth gate for the SearXNG proxy and /inference, but it
was written to a predictable temp path with default permissions, so any local
user could read it and mint valid requests. Creating it with 0600 closes that,
and the chmod covers a file left behind by an earlier build, since mode only
applies when the file is created.
Also drops the older regenerateSearchToken test: its assertions (length > 2,
lowercase alphanumeric) are both implied by the /^[0-9a-f]{64}$/ check in the
CSPRNG test.
The digest is the part a caller without the token would have to guess to forge a ?token=, so an 8-byte one capped forgery resistance at 64 bits regardless of how much entropy the token itself carries. Adds the first tests for this module: a real argon2 round trip through the server's own verification call, the digest length, and the cached-hash reuse path. No migration needed, since every build mints a new token and cached hashes stop verifying with it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to #2217, which made the search token a CSPRNG secret. Two things still capped what that secret buys, plus a test cleanup.
The token file was world-readable.
regenerateSearchToken()wrote to the predictable path{os.tempdir()}/minisearch-tokenwith default permissions (0666 & ~umask), so on a shared host any local user could read the token and mint valid/search/*and/inferencerequests, no matter how the bytes were generated. It's now created with0600, plus achmodSyncright after, becausemodeonly applies when the file is created and a token file left behind by an earlier build would otherwise keep its old permissions.The argon2 digest was 8 bytes. The digest is the part a caller without the token would have to guess to forge a
?token=, so it sets the ceiling on forgery resistance: 8 bytes capped it at 64 bits, whatever the token's own entropy.hashLengthinclient/modules/searchTokenHash.tsis now 32. Cost is unchanged (hashLengthdoesn't affect argon2's work, only the output length), and the URL grows by about 32 characters.Test cleanup: the older
regenerateSearchToken should write a new tokentest is gone, since its assertions (length > 2, lowercase alphanumeric) are both implied by the/^[0-9a-f]{64}$/check in the CSPRNG test from #2217.One suggestion from the review of #2217 is deliberately not here: replacing that PR's
afterEach(vi.restoreAllMocks)with a scopedmockRestore().client/modules/notifications.test.tsalready uses theafterEachform, so the file matches the existing convention, and the hook still runs when a test fails before reaching a manual restore. Leaving it alone.0600, pluschmodSyncfor files from earlier buildsserver/searchToken.tsclient/modules/searchTokenHash.tsserver/searchToken.test.tsclient/modules/searchTokenHash.test.tsdocs/security.mdType of Change
Checklist
npm run lintpassesnpm run test), with tests added where it made senseHow to test
npm run test(31 files, 282 tests). Both new tests fail on the pre-change code for the right reason:expected undefined to deeply equal { mode: 384 }andexpected 8 to be 32.npm run build, then check the token file:ls -l "$(node -e "import('temp-dir').then(m => console.log(m.default))")/minisearch-token". It should be-rw-------and 64 bytes.chmod 0666that file, runnpm run buildagain, and it should be back to-rw-------.Security, performance, or breaking changes? Expand if relevant.
Security-positive, no migration needed.
The local-read exposure only mattered on hosts where another user shares the temp directory; single-user machines and one-app containers were never affected. Deployments pick both changes up on their next build.
The wider digest needs no migration because every build mints a new token, so a cached
lastSearchTokenHashfrom a previous build stops verifying anyway and gets regenerated at 32 bytes. Old 8-byte hashes would still be accepted if the token were unchanged (argon2 reads the parameters from the encoded hash), which is why this doesn't reject short digests explicitly.One new failure mode worth naming: if the token file is owned by a different user,
chmodSyncnow throwsEPERMand the build fails, where before it would silently overwrite that user's file. That's the vulnerable case, so failing loudly is the intent, not a regression to work around.Verified beyond the unit tests: a real
npm run buildon macOS produced a 64-byte0600file and repaired a file I had set to0666beforehand; and a client-produced hash with the new parameters was accepted by the realverifyTokenAndRateLimit()against the token that build wrote (garbage still gets a 401). No live SearXNG round trip, since the instance here returns 503.