fix(redis): refuse commands that move the shared connection - #1121
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Aditya-XR
force-pushed
the
fix/redis-query-session-state
branch
from
September 27, 2026 08:54
5e3e160 to
78ba50f
Compare
The provider is cached per connection for the whole process and runs
every statement on one client, so a SELECT sent through query() changed
the database every later request on that connection read. RESET and
AUTH also logged the client back in as `default`, and HELLO 3 broke
reply parsing.
runCommand now refuses SELECT, RESET, AUTH and HELLO by command name,
so a SELECT queued inside MULTI is refused as well. The message points
to the Keys panel database picker and the connection's Database field.
SWAPDB, MOVE and redis.call('SELECT') from a script were measured and
leave the connection unchanged, so they still run.
Closes libredb#1107
…nnection The check matched the whole command word, but redis 8.10.2 matches a name only up to a NUL when it repeats the connection's previous command, and the provider sends SELECT <db> on connect. So SELECT\0 0 as the first statement on a fresh provider moved it to database 0. Every word is now compared up to its first NUL. It also refuses the commands after which the shared client stops answering (QUIT, SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE, MONITOR, CLIENT REPLY) and the ones that hold it until data or a timeout arrives (BLPOP, BRPOP, BRPOPLPUSH, BLMOVE, BLMPOP, BZPOPMIN, BZPOPMAX, BZMPOP, WAIT, WAITAOF, and XREAD or XREADGROUP with BLOCK). All measured on redis 8.10.2 through ioredis 5.11.1; docs/providers/redis.md 5.2b lists them and what still runs.
cevheri
force-pushed
the
fix/redis-query-session-state
branch
from
September 27, 2026 22:11
78ba50f to
609f31e
Compare
Member
|
Thanks @Aditya-XR, your measurements held up on a live Redis 8.10.2, including the read-only user turning into
§5.2b in |
6 tasks
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
query()now refuses the Redis commands that change the connection itself rather than the data:SELECT,RESET,AUTHandHELLO.The provider is cached per connection for the whole process and runs every statement on one client, so a
SELECT 0sent throughPOST /api/db/querymoved every later request on that connection to database 0, whoever sent it. Measured on Redis 8.10.2 through ioredis 5.11.1, on a connection configured for database 2 as a read-only ACL user (+@read):SELECT 0GETs read database 0 instead of the configured databaseMULTI/SELECT 0/EXECRESETdefault:SETfailed withNOPERMbefore, returnedOKafterAUTH default <anything>RESETagainst a stockdefault nopassserverHELLO 3Protocol error, got "%"SWAPDB,MOVEand a script'sredis.call('SELECT', n)were measured too and leave the connection's database and user unchanged, so they still run.I chose refusing over an isolated
withDatabaseclient per statement: that would add a connect to every run, and aSELECTon a client that is closed right after would have no lasting effect anyway. The error message points to the Keys panel database picker (which reads through the per-rundatabasefield from #1095, on a provider of its own) and to the connection's Database field.Type of Change
Related Issue
Closes #1107
Changes Made
src/lib/db/providers/keyvalue/redis.ts:SESSION_STATE_COMMANDSand a check at the top ofrunCommand. It matches the upper-cased command name, so the plain form, the JSON form and aSELECTinsideMULTIare all refused before anything reaches the client.tests/integration/db/redis-provider.test.ts: the ioredis mock'scall("SELECT", n)now moves the mock connection the way the server does. Atest.eachsendsSELECT 0,{"command":"select","args":["0"]},RESET,AUTH default xandHELLO 3throughquery()on a database-2 connection and asserts the command is refused, nothing reached the client, and the next query still runs on database 2.src/lib/api/object-route.ts: updated the threeredis.tsline citations that moved (checked by the existing citation guard test).docs/providers/redis.md: new §5.2b stating the behaviour, the measurements and where to pick a database instead.Testing
The five new tests fail on
mainand pass with the change.redis-provider.test.ts: 223 pass.redis.tsis at 100% lines.bun run typecheckis clean andbun run lintreports 0 errors (no new warnings). I also ran the patched provider against a live Redis 8.10.2, on a connection configured for database 2, as bothdefaultand a read-only ACL user (~* +@read +ping +info +@connection). Every statement in the table above was refused, including aSELECT 0queued insideMULTI, and after each oneGET probe:db2onlystill answeredin-two. The read-only user still gotNOPERMonSETafterwards.In the full
bun run teston Windows, the only failures are the node:sqlite driver test,launcher-utils(bind address / startup URL /extractArchive) andpack-standalone-tarball. All of them fail the same way onmainon Windows.Test Environment
mainat 97eec34)Checklist
bun run test:coverageandbun run coverage:check)src/lib/db/providers/, I updated the matchingdocs/providers/documentation andtests/integration/db/tests in the same PR (provider triad)Additional Notes
About the coverage box: on Windows,
test:coveragegoes red on the Windows-only failures above and so skips the merge. Merging the per-file reports by hand and runningscripts/check-coverage.mjson the result gives 63716/63721 lines. The only uncovered lines aresrc/instrumentation.ts32-36, and the test that covers them isskipIf(process.platform === "win32"). I expect the gate to pass on CI's Linux runner, so I left the box for CI to confirm.