Skip to content

fix(redis): refuse commands that move the shared connection - #1121

Merged
cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/redis-query-session-state
Sep 27, 2026
Merged

cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/redis-query-session-state

Conversation

@Aditya-XR

@Aditya-XR Aditya-XR commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

query() now refuses the Redis commands that change the connection itself rather than the data: SELECT, RESET, AUTH and HELLO.

The provider is cached per connection for the whole process and runs every statement on one client, so a SELECT 0 sent through POST /api/db/query moved 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):

Command Effect on the shared client before this change
SELECT 0 Later GETs read database 0 instead of the configured database
MULTI / SELECT 0 / EXEC Same as above
RESET Moved to database 0 and re-authenticated as default: SET failed with NOPERM before, returned OK after
AUTH default <anything> Same as RESET against a stock default nopass server
HELLO 3 Switched the reply protocol; ioredis then failed with Protocol error, got "%"

SWAPDB, MOVE and a script's redis.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 withDatabase client per statement: that would add a connect to every run, and a SELECT on 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-run database field from #1095, on a provider of its own) and to the connection's Database field.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test addition or update

Related Issue

Closes #1107

Changes Made

  • src/lib/db/providers/keyvalue/redis.ts: SESSION_STATE_COMMANDS and a check at the top of runCommand. It matches the upper-cased command name, so the plain form, the JSON form and a SELECT inside MULTI are all refused before anything reaches the client.
  • tests/integration/db/redis-provider.test.ts: the ioredis mock's call("SELECT", n) now moves the mock connection the way the server does. A test.each sends SELECT 0, {"command":"select","args":["0"]}, RESET, AUTH default x and HELLO 3 through query() 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 three redis.ts line 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

  • I have tested this locally
  • I have added/updated tests
  • All existing tests pass

The five new tests fail on main and pass with the change. redis-provider.test.ts: 223 pass. redis.ts is at 100% lines. bun run typecheck is clean and bun run lint reports 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 both default and a read-only ACL user (~* +@read +ping +info +@connection). Every statement in the table above was refused, including a SELECT 0 queued inside MULTI, and after each one GET probe:db2only still answered in-two. The read-only user still got NOPERM on SET afterwards.

In the full bun run test on Windows, the only failures are the node:sqlite driver test, launcher-utils (bind address / startup URL / extractArchive) and pack-standalone-tarball. All of them fail the same way on main on Windows.

Test Environment

  • LibreDB Studio Version: 0.16.2 (main at 97eec34)
  • Browser: N/A (provider and route change)
  • OS: Windows 11
  • Node.js/Bun Version: Node 22.17.1 / Bun 1.4.2
  • Database Type: Redis 8.10.2 (ioredis 5.11.1)

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The required CI test job passes the 100% line-coverage gate (bun run test:coverage and bun run coverage:check)
  • If I changed src/lib/db/providers/, I updated the matching docs/providers/ documentation and tests/integration/db/ tests in the same PR (provider triad)
  • Any dependent changes have been merged and published

Additional Notes

About the coverage box: on Windows, test:coverage goes red on the Windows-only failures above and so skips the merge. Merging the per-file reports by hand and running scripts/check-coverage.mjs on the result gives 63716/63721 lines. The only uncovered lines are src/instrumentation.ts 32-36, and the test that covers them is skipIf(process.platform === "win32"). I expect the gate to pass on CI's Linux runner, so I left the box for CI to confirm.

Copilot AI balanced review requested due to automatic review settings September 25, 2026 09:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri added bug Something isn't working database-provider redis labels Sep 25, 2026
@Aditya-XR
Aditya-XR force-pushed the fix/redis-query-session-state branch from 5e3e160 to 78ba50f Compare September 27, 2026 08:54
Aditya-XR and others added 2 commits September 28, 2026 01:04
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
cevheri force-pushed the fix/redis-query-session-state branch from 78ba50f to 609f31e Compare September 27, 2026 22:11
@cevheri

cevheri commented Sep 27, 2026

Copy link
Copy Markdown
Member

Thanks @Aditya-XR, your measurements held up on a live Redis 8.10.2, including the read-only user turning into default after RESET. To get this in cleanly I rebased the branch on main (force-pushed; your commit only changed in the three citations) and added one commit on top:

  1. SELECT\0 0 got past the name check. Redis 8.10.2 matches a command name only up to a NUL when it repeats the connection's previous command, and openClient sends SELECT <db> on connect, so on a fresh provider it moved the connection to database 0 through POST /api/db/query. Every command word is now compared up to its first NUL.
  2. The same shared client stops answering after QUIT, SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE, MONITOR and CLIENT REPLY, and is held by blocking reads (the BLPOP family, WAIT, WAITAOF, XREAD / XREADGROUP with BLOCK), so those are refused as well. Only SELECT keeps the Keys panel hint in its message.
  3. The object-route.ts citations are recounted for main, where feat(db): declare the container paths an engine accepts, so the route and the provider refuse by one rule #1148 had moved them.

§5.2b in docs/providers/redis.md has the measurements and what still runs. The tests add the NUL forms and controls, such as a stream key or group named BLOCK, which still run. All gates, 100% coverage, build, and a route check on a fresh server pass locally.

@cevheri
cevheri merged commit e10d515 into libredb:main Sep 27, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working database-provider redis

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redis: a SELECT sent through the query route moves the shared provider to another database

3 participants