Skip to content

[Security] Auto-updater downloads + executes binary with no integrity verification (no sha256/signature) and uses unfiltered tarfile.extractall #2273

@ktwu01

Description

@ktwu01

Summary

The bundled-binary auto-updater (src/kimi_cli/ui/shell/update.py) downloads a tarball from https://cdn.kimi.com/binaries/kimi-cli/{version}/..., extracts it with unfiltered tarfile.extractall, copies the kimi binary out, marks it executable, and the user runs it on next launch — all without verifying:

  • the SHA-256 of the tarball against a published manifest,
  • a signature (minisign / cosign / GPG) over the tarball or the binary,
  • or even the byte length against a manifest.

HTTPS protects the channel against passive MITM, but it does not protect against:

  1. A compromise or misconfiguration of cdn.kimi.com (cache poisoning, accidental publish of a wrong artifact).
  2. A user/enterprise environment with a custom root CA installed (corporate proxies, mitmproxy testing setups, malware-installed root CAs).
  3. A future migration to a different CDN where TLS settings differ.

The first one is the real concern: a single bad publish silently pushes a compromised binary to every kimi-cli user on next auto-update. Because the binary is then chmod +x and executed as the user, this is straightforwardly RCE-on-end-user equivalent.

This pattern is well-understood; modern release tooling (GoReleaser, cargo-dist, Homebrew bottles, npm-with-provenance) all ship a manifest with sha256 sums + optional signatures specifically to break this trust dependency.

Secondary issue (smaller): unfiltered tarfile.extractall(tmpdir)

Python 3.12 deprecated extractall() without an explicit filter= argument; Python 3.14 (which .python-version pins) emits a DeprecationWarning, and a future release will raise. CVE-2007-4559 (the original "tar slip" bug) made filter='data' the recommended default — even for trusted sources, since it's defense-in-depth and silences the deprecation:

- tar.extractall(tmpdir)
+ tar.extractall(tmpdir, filter="data")  # PEP 706

I noticed kimi-cli's other archive-extraction sites (src/kimi_cli/cli/plugin.py:77, src/kimi_cli/vis/api/sessions.py:657) both have explicit path-traversal checks before extractall. The auto-updater is the lone outlier.

Repro

I have not exploited this — there is nothing to exploit on a healthy cdn.kimi.com. The repro is structural: read src/kimi_cli/ui/shell/update.py and search for any call into hashlib, cryptography, signify, minisign, gpg, etc. There are none. The download → extract → chmod +x → execute path runs unconditionally if the HTTPS download itself succeeds.

Proposed fix

Two parts.

1. Publish + verify a sha256 manifest. Alongside each kimi-{version}-{target}.tar.gz on the CDN, publish kimi-{version}-{target}.tar.gz.sha256 (and ideally a single manifest.json per version with all targets). The updater fetches both, verifies, and only then extracts:

import hashlib

# After downloading tar to tar_path, also fetch the .sha256 sidecar.
async with session.get(download_url + ".sha256") as resp:
    resp.raise_for_status()
    expected_sha256 = (await resp.text()).strip().split()[0]

actual_sha256 = hashlib.sha256(Path(tar_path).read_bytes()).hexdigest()
if actual_sha256 != expected_sha256:
    logger.error(
        "Checksum mismatch for {url}: expected {expected}, got {actual}",
        url=download_url, expected=expected_sha256, actual=actual_sha256,
    )
    _print("[red]Update aborted: checksum mismatch.[/red]")
    return UpdateResult.FAILED

2. (Stronger, optional follow-up) sign the manifest with a long-lived release key (cosign / minisign / sigstore). This protects against CDN compromise even if the attacker can replace both the tarball and its .sha256 sidecar.

3. Add filter="data" to the extractall call as a 1-line defense-in-depth + Python-3.14 future-proof fix.

Why I'm filing this publicly

There's no live exploit to disclose privately — this is a structural defense-in-depth gap, observable directly from the public source. Auto-updaters are a textbook supply-chain target, and a 30-line defensive change today saves you from a hard-to-detect incident later. Happy to send a PR if you'd like — I'd start with the sha256 sidecar since it doesn't require key management.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions