Conversation
sortGetKeys() scans the SORT arguments for the STORE option to report the destination key to the ACL layer, to COMMAND GETKEYS and to the cluster slot check. When it found a STORE it recorded the destination position but left the scan cursor on the STORE token, so the next iteration examined the destination name as if it were an option keyword. A destination named "by" or "get" therefore consumed the argument after it and "limit" consumed two, hiding a later STORE clause. A destination named "store" was taken for another STORE clause, reporting whatever followed it instead. Either way the reported key and the key SORT writes are different, because SORT keeps the last STORE. Permissions were checked against the reported key while the write landed on the real one, so a user could write outside their allowed key patterns. COMMAND GETKEYS reported the wrong key, and in cluster mode the destination was added to the reported key's slot, leaving a key that KEYS lists but EXISTS cannot find. Advance the cursor past the destination so a key name can no longer be reparsed as an option. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
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.
When the server works out which keys a
SORTtouches, it re-examines theSTOREdestination as if that key name were an option keyword. A destination namedby,getorlimittherefore swallows the arguments after it and hides a laterSTOREclause, and one namedstoreis read as anotherSTOREclause and reports whatever follows it. BecauseSORTwrites to the lastSTORE, the server ends up checking permissions against one key and writing to a different one, so a user can write to a key outside their allowed patterns. The fix advances the scan past the destination so a key name is never re-read as an option.Details
Problem
sortGetKeys()(src/db.c:2879) walksSORT's arguments looking forSTORE, because the destination is the one key that can appear anywhere in the argument list. Options that take arguments are stepped over using a table:When the walk found
STOREit recorded the destination position and left the inner loop without moving the cursor (src/db.c:2884-2891):iis still on theSTOREtoken, so the next turn offor (i = 2; i < argc; i++)lands on the destination name and tests it against the skiplist and against"store". Two shapes follow:byorgetconsumes one further argument,limitconsumes two. When a secondSTOREclause is inside that window, the walk never sees it.storematches the"store"test itself, so the next token is recorded as the destination instead. This needs no secondSTOREat all.The executor has neither problem.
sortCommandGeneric()(src/sort.c) steps past the destination, and it deliberately keeps the lastSTORErather than the first:So the key the server authorizes and the key it writes are two different keys.
Impact
Key permissions are applied to the reported key, not the real destination. Two shapes, both against an unpatched server:
forbidden:dstandstoreare both outside~allowed:*. The decoys (~get,~alpha) are granted only so that the decoy itself passes, which is what leaves the real destination as the sole reason to reject the command. After the fix both sequences returnNOPERM No permissions to access a key,EXISTSis0, andACL DRYRUNnames the right key:User u has no permissions to access the 'store' key.The same key list feeds
getNodeByQuery()(src/cluster.c:984). When the decoy hashes to the source's slot the cross-slot check passes and the destination is added to the source's slot index instead of its own, leaving a keyKEYSlists butEXISTScannot find:With the fix that command is rejected with
CROSSSLOT Keys in request don't hash to the same slot.SORT_ROis unaffected: it rejectsSTOREat execution andsortROGetKeys()never scans for it.Behavior
Destination reported by
COMMAND GETKEYS sort src <args>, measured against both builds, next to the keySORTactually creates:SORT srcSTORE dstdstdstdstSTORE a STORE dstdstdstdstSTORE by STORE dstbydstdstSTORE get STORE dstgetdstdstSTORE limit STORE dstlimitdstdstSTORE store alphaalphastorestoreSTORE STORE alphaalphaSTORESTORESTORE store by w_*bystorestoreSTORE bybybybyBY w_* GET p_* STORE dstdstdstdstLIMIT 0 5 STORE dstdstdstdstSTOREThe destination names that derail the walk are exactly the tokens it recognizes:
by,get,limitandstore, in any case.Why the existing tests missed it
tests/unit/sort.tcl:119already covers repeatedSTORE:test "SORT extracts multiple STORE correctly" { r command getkeys sort abc store invalid store stillbad store def } {abc def}invalidandstillbadare not tokens the walk recognizes, so the misplaced cursor lands on a word it ignores and the walk recovers on the next argument. The assertion only ever used destinations that cannot trigger the bug.Fix
One statement, in the
storebranch:numis still not incremented andfound_storeis untouched, so last-one-wins is preserved and the reported key count is unchanged. The branch is already guarded byi + 1 < argc, so the extra increment leavesi <= argc - 1, andkeys[num].pos <= argc - 1always.georadiusGetKeys()(src/db.c:2974) already does exactly this after recording its ownSTORE/STOREDISTdestination.The
storebranch ofsortGetKeys()differs across release branches only inargv[i]->ptr(8.04bf1df644, 8.155a542671, 9.0a100149d5) versusobjectGetVal(argv[i])(9.17f1dffedf, unstableda91ccd12), which the added line does not touch, so the hunk applies to all of them unchanged.Alternative considered
Have
sortGetKeys()sharesortCommandGeneric()'s argument walk so the two cannot drift apart again. That is the right long-term shape, but it is a large change to a function that has to cherry-pick into four release branches, and the two walks legitimately differ: the key extractor must not validateLIMIT's integers and must not rejectBYpatterns in cluster mode, and it is called from inside the ACL check (ACLSelectorCheckCmd(),src/acl.c:1968) thatsortCommandGeneric()itself depends on for itsBY/GETdecision. Worth doing separately onunstable.Rejecting a repeated
STOREas a syntax error would also close this and is arguably the cleaner command definition, but it is a user-visible behavior change to a shipped command and cannot be backported.Breaking out of the outer loop on the first
STOREis shorter than skipping the destination, but it reports the first destination instead of the last, which is the same bug mirrored: the server would authorize a key it does not write.Decisions for a reviewer
8.0,8.1,9.0and9.1. Proposal: yes, all four, unchanged.STOREshould become a syntax error. Proposal: separate issue, not this patch.Testing
With
src/db.creverted toda91ccd12and both new tests kept in the tree:Each assertion fails on its own, not just the first. On the unpatched build the
storeshapes reportabc alpha,abc alphaandabc bywhere the test expectsabc store,abc STOREandabc store, andSORT allowed:src STORE store alphareturns3withEXISTS storeat1.Completeness is the part worth checking for this one, since a partial fix would leave a smaller version of the same hole. Enumerating every option sequence up to length 5 over
{STORE, store, by, get, limit, alpha, dst, w_*, 0}against real servers, and comparing the reported destination to the key the server actually creates:4816and3402being identical on both builds is the other half: the same commands succeed and write to the same keys, so this only changes what gets reported.The ACL test lives in
acl-v2.tclbeside the existingTest sort with ACL permissions, and uses its own client because the cleanupACL deluserkills whichever client is authenticated as that user.This was generated by AI but verified, with love, by a human.