Skip to content

feat: add SHA-256 checksum verification to install.js#592

Merged
MaxHuang22 merged 7 commits intolarksuite:mainfrom
MaxHuang22:feat/install-checksum-verification
Apr 23, 2026
Merged

feat: add SHA-256 checksum verification to install.js#592
MaxHuang22 merged 7 commits intolarksuite:mainfrom
MaxHuang22:feat/install-checksum-verification

Conversation

@MaxHuang22
Copy link
Copy Markdown
Collaborator

@MaxHuang22 MaxHuang22 commented Apr 21, 2026

Summary

Downloaded binary archives in scripts/install.js were not verified against any checksum, leaving the install path vulnerable to CDN tampering, MITM, or corruption. This PR bundles GoReleaser's checksums.txt in the npm package and verifies the SHA-256 of every downloaded archive before extraction.

Changes

  • Restructure scripts/install.js to be side-effect-free on require() via require.main === module guard
  • Add getExpectedChecksum(archiveName, checksumsDir?) to parse GoReleaser checksums.txt format
  • Add verifyChecksum(archivePath, expectedHash) using streaming 64KB-chunk hash to avoid loading entire archive into memory; [SECURITY]-prefixed error on mismatch
  • Add assertAllowedHost(url) host allowlist (github.com, objects.githubusercontent.com, registry.npmmirror.com) and --max-redirs 3 to curl args in download()
  • Integrate checksum verification into install() flow (after download, before extraction)
  • Add checksums.txt to package.json files array
  • Harden .github/workflows/release.yml publish-npm job: use GITHUB_REF_NAME instead of parsing package.json, add explicit test -s checksums.txt guard
  • Create scripts/install.test.js with 16 unit tests covering all exported functions

Design Decisions

  • Fail-open on missing checksums.txt — intentional per spec §4.2. The missing-file scenario in production is CI pipeline failure, and the release workflow's test -s checksums.txt guard provides a compensating fail-closed control there. Switching client-side to fail-closed can follow once the pipeline is proven stable.
  • Host allowlist only gates the initial URL — curl's redirect target is not re-validated, but checksum verification makes this acceptable defense-in-depth rather than a primary control.
  • Streaming hash — archives are typically 10-30MB but can grow; streaming with 64KB chunks keeps memory usage constant regardless of archive size.

Test Plan

  • make unit-test passed
  • make validate passed (build, vet, unit test, integration test)
  • skipped: local-eval E2E (pure JavaScript change, no Go CLI commands affected)
  • skipped: local-eval skillave (no shortcut/skill changes)
  • acceptance-reviewer passed (5/5 cases)
  • manual verification: node --test scripts/install.test.js (16/16 pass), node -e "require('./scripts/install.js')" (side-effect-free)

Related Issues

N/A

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cryptographic verification of downloaded packages to ensure integrity and authenticity.
    • Enhanced download security with strict hostname validation and limited redirects.
  • Tests

    • Introduced comprehensive test coverage for package verification and security validation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Adds checksum-based integrity checks for published CLI binaries: release workflow now downloads checksums.txt, package.json includes it for publishing, scripts/install.js verifies SHA‑256 and restricts download hosts (with deferred execution and exported helpers), and new tests validate checksum helpers and host checks.

Changes

Cohort / File(s) Summary
Release Workflow
\.github/workflows/release.yml
Adds an authenticated step in the publish-npm job to download checksums.txt from the GitHub release for the current tag and fail the job if the file is missing or empty.
Package Configuration
package.json
Adds checksums.txt to the published files array so the checksums are bundled with the npm package.
Installation Script
scripts/install.js
Adds hostname allowlist (assertAllowedHost), limited curl redirects, functions to read expected SHA‑256 (getExpectedChecksum) and verify archive (verifyChecksum), defers side effects behind require.main === module, moves binDir creation into install(), and exports checksum/host helpers.
Installation Tests
scripts/install.test.js
New node:test unit tests covering getExpectedChecksum, verifyChecksum, and assertAllowedHost behaviour (matching, missing files, malformed lines, case-insensitivity, host normalization, and errors).

Sequence Diagram

sequenceDiagram
    participant CI as CI (Release)
    participant GH as GitHub Releases
    participant NPM as npm Registry
    participant User as Developer Machine
    participant Install as install.js
    participant Curl as curl
    participant FS as File System

    CI->>GH: Authenticate and download `checksums.txt` for tag
    CI->>NPM: Publish package (includes `checksums.txt`)

    User->>NPM: npm install `@larksuite/cli`
    NPM-->>User: Package (with `checksums.txt`)
    User->>Install: Run postinstall (node scripts/install.js)

    Install->>Install: assertAllowedHost(downloadUrl)
    Install->>Curl: Download archive (--max-redirs 3)
    Curl-->>Install: Archive file
    Install->>FS: Read `checksums.txt`
    Install->>Install: compute SHA-256(archive)
    Install->>Install: compare with expected hash

    alt Match
        Install->>FS: extract archive to bin
        Install-->>User: installation success
    else Missing or Mismatch
        Install-->>User: throw `[SECURITY] Checksum mismatch` / error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • liangshuo-1

Poem

🐰 I hop through CI fields so bright,
I fetch the checksums, guard the night,
I sniff allowed hosts and tally the hash,
I block the sneaks and guard every stash,
Safe installs now — hop, twirl, and delight! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main security enhancement—adding SHA-256 checksum verification to the install script.
Description check ✅ Passed The description is comprehensive and exceeds template requirements with clear sections covering summary, detailed changes, design decisions, thorough test plan, and related issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/M Single-domain feat or fix with limited business impact label Apr 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.06%. Comparing base (fbed6be) to head (8064215).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #592      +/-   ##
==========================================
+ Coverage   60.92%   62.06%   +1.13%     
==========================================
  Files         399      407       +8     
  Lines       34089    35799    +1710     
==========================================
+ Hits        20770    22217    +1447     
- Misses      11395    11554     +159     
- Partials     1924     2028     +104     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@80642154212743408792c6c57e7691cfd19604b6

🧩 Skill update

npx skills add MaxHuang22/cli#feat/install-checksum-verification -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/install.js (1)

42-61: ⚠️ Potential issue | 🟠 Major

Validate redirect targets against the host allowlist, or disable automatic redirects.

The assertAllowedHost() check at line 50 only validates the initial URL. However, curl --location will follow Location headers to other hosts without validation. While --max-redirs 3 limits the redirect count, curl has no built-in option to enforce host allowlisting on redirects (--proto-redir controls protocol types, not hosts). Either validate each redirect target or use --location-trusted with explicit protocol constraints, or remove --location to reject redirects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install.js` around lines 42 - 61, The current download function uses
curl with "--location" so redirects are followed without re-checking hosts;
update the download(url, destPath) implementation to remove the "--location"
flag from args and implement manual redirect handling: perform the request, on
3xx responses read the Location header, call assertAllowedHost(newUrl) before
following, enforce the existing "--max-redirs" limit (or the args' max redirs
value) and other timeout options, and continue following only when the
redirected host is allowed; preserve the isWindows check that adds
"--ssl-revoke-best-effort" and keep using execFileSync("curl", ...) for each
request/redirect step.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-04-21-install-checksum.md`:
- Around line 150-152: The fenced expected-output block containing [
'getExpectedChecksum', 'verifyChecksum' ] should be labeled as a text fence for
markdown linting; update the code fence that wraps getExpectedChecksum and
verifyChecksum so it begins with ```text and ends with ``` to mark it as plain
text output.

In `@docs/superpowers/specs/2026-04-21-install-checksum-design.md`:
- Around line 7-21: The fenced code blocks in the spec (the trust-chain diagram
starting with "GoReleaser (CI) generates dist/checksums.txt", the install flow
block containing "download archive (github, fallback mirror) -> expectedHash =
getExpectedChecksum(archiveName)", and the test-case outline starting with
"getExpectedChecksum - returns correct hash...") are unlabeled and trigger
markdownlint; add the language label "text" to each triple-backtick fence for
those blocks (and the other unlabeled fences noted around the similar sections)
so they read ```text ... ``` to silence the linter while preserving content.

In `@scripts/install.js`:
- Around line 106-110: The installer currently returns null when checksums.txt
is missing (the fs.existsSync(checksumsPath) branch), allowing installs to
proceed unverified; change this to fail-closed by throwing an Error or exiting
non-zero with a clear message when checksumsPath is missing, and add an explicit
opt-out (e.g., read an env var like SKIP_CHECKSUMS=true or a CLI flag) so
callers of the verification logic (the checksum verification flow that expects
the checksums content) can opt out for local dev; update the log text to reflect
the required opt-out variable and ensure callers no longer treat null as "skip"
but instead propagate the error.

---

Outside diff comments:
In `@scripts/install.js`:
- Around line 42-61: The current download function uses curl with "--location"
so redirects are followed without re-checking hosts; update the download(url,
destPath) implementation to remove the "--location" flag from args and implement
manual redirect handling: perform the request, on 3xx responses read the
Location header, call assertAllowedHost(newUrl) before following, enforce the
existing "--max-redirs" limit (or the args' max redirs value) and other timeout
options, and continue following only when the redirected host is allowed;
preserve the isWindows check that adds "--ssl-revoke-best-effort" and keep using
execFileSync("curl", ...) for each request/redirect step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ccca54a-0a7d-40a9-b9dd-c580b973148e

📥 Commits

Reviewing files that changed from the base of the PR and between 04e3a28 and 33f4b8d.

📒 Files selected for processing (6)
  • .github/workflows/release.yml
  • docs/superpowers/plans/2026-04-21-install-checksum.md
  • docs/superpowers/specs/2026-04-21-install-checksum-design.md
  • package.json
  • scripts/install.js
  • scripts/install.test.js

Comment thread docs/superpowers/plans/2026-04-21-install-checksum.md Outdated
Comment thread docs/superpowers/specs/2026-04-21-install-checksum-design.md Outdated
Comment thread scripts/install.js
Change-Id: I5444e3f34642d7c0740b6422a70ca6921a85e363
Change-Id: I87548be25d30c384e743da17b1d161b9d9f0ea87
Change-Id: Ifc2067bf1b824b02257dba7b53716fbe18d0f6b6
Change-Id: I2580782866049f1f62a2597e86b7bf59d0e50925
Change-Id: I2d7c44d9d5b9075158f63c0f8cf66c1e0abe3d8d
@MaxHuang22 MaxHuang22 force-pushed the feat/install-checksum-verification branch from 33f4b8d to 537967b Compare April 21, 2026 13:05
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 48-53: The release workflow step that downloads checksums is
fragile: it derives the tag from package.json and doesn't fail if checksums.txt
is missing, which can silently disable verification in getExpectedChecksum
(scripts/install.js). Update the GitHub Action to use the triggering tag
(github.ref_name) instead of reading package.json, and add an explicit check
after gh release download to fail the job if checksums.txt is not present (exit
nonzero and log a clear error). Optionally add a follow-up step that runs the
install verification (e.g., invoke the verification path in scripts/install.js
or a dry-run installation) to ensure checksums.txt actually validates artifacts
before publishing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a8a3355-5498-49d6-8fee-b9c11af93389

📥 Commits

Reviewing files that changed from the base of the PR and between 33f4b8d and 537967b.

📒 Files selected for processing (4)
  • .github/workflows/release.yml
  • package.json
  • scripts/install.js
  • scripts/install.test.js
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • scripts/install.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/install.js

Comment thread .github/workflows/release.yml Outdated
…orkflow

Address CodeRabbit review: use GITHUB_REF_NAME instead of parsing
package.json to avoid version drift, and add explicit file check to
fail loudly if checksums.txt is missing or empty.

Change-Id: I8a5658412b6afc338ad2a642baba146cceafd0fc
- verifyChecksum: switch from readFileSync to streaming 64KB chunks
  to avoid loading entire archive (10-100MB) into memory
- Export and test assertAllowedHost: 7 cases covering allowed hosts,
  rejection, case normalization, port handling, invalid URL
- Add ALLOWED_HOSTS comment clarifying it only gates initial URL
- Add getExpectedChecksum tests for malformed/tab-separated lines

Change-Id: Ida639def89c242b3b261a76effae08fd414a10dc
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
scripts/install.js (1)

106-129: Parser handles GoReleaser format correctly; minor parameter-shadowing nit.

Using indexOf(" ") as the hash/name separator correctly rejects tab-separated and single-space malformed lines (matching the new tests). The fail-open on missing checksums.txt is intentional per spec §4.2.

Nit: the parameter name archiveName shadows the module-level constant on line 39, which can be mildly confusing when reading install() call sites. Renaming the parameter (e.g., name) is optional.

♻️ Optional rename to avoid shadowing
-function getExpectedChecksum(archiveName, checksumsDir) {
+function getExpectedChecksum(name, checksumsDir) {
   const dir = checksumsDir || path.join(__dirname, "..");
   const checksumsPath = path.join(dir, "checksums.txt");
@@
-    const name = trimmed.slice(idx + 2);
-    if (name === archiveName) return hash;
+    const entryName = trimmed.slice(idx + 2);
+    if (entryName === name) return hash;
   }
 
-  throw new Error(`Checksum entry not found for ${archiveName}`);
+  throw new Error(`Checksum entry not found for ${name}`);
 }

Based on learnings, the null-return when checksums.txt is missing is intentional (spec §4.2, CI test -s guard as the publish-time safety net) and is deliberately not being flagged here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install.js` around lines 106 - 129, getExpectedChecksum currently
uses a parameter named archiveName which shadows the module-level constant
archiveName; rename the parameter (e.g., to name or archiveFile) and update all
references inside getExpectedChecksum and any call sites (e.g., install() calls
that pass the archiveName) to use the new parameter name so there is no
shadowing between the function parameter and the module-level archiveName
constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/install.js`:
- Around line 106-129: getExpectedChecksum currently uses a parameter named
archiveName which shadows the module-level constant archiveName; rename the
parameter (e.g., to name or archiveFile) and update all references inside
getExpectedChecksum and any call sites (e.g., install() calls that pass the
archiveName) to use the new parameter name so there is no shadowing between the
function parameter and the module-level archiveName constant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be5fc262-b14b-4468-822f-98e83eae616a

📥 Commits

Reviewing files that changed from the base of the PR and between 2a19f0c and 8064215.

📒 Files selected for processing (2)
  • scripts/install.js
  • scripts/install.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/install.test.js

@MaxHuang22 MaxHuang22 merged commit 593025d into larksuite:main Apr 23, 2026
18 checks passed
@liangshuo-1 liangshuo-1 mentioned this pull request Apr 23, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants