Mongo2Go 4.2.0: security updates, Linux ARM64, smaller package#161
Merged
Conversation
FluentAssertions 8.x moved to the Xceed Community License, which restricts non-commercial use. AwesomeAssertions is the community fork under Apache-2.0, preserving the original attribution. Audited before adoption: - provenance chain verified: nuspec commit == PDB SourceLink commit == GPG-verified commit in the public repository - Apache-2.0 in both the nuspec and the repository - no build-time hooks (.targets/.props/scripts) - no transitive dependencies on net8.0 - static analysis: no network, process, or registry API surface - no security advisories Pinned to 9.4.0 rather than 9.5.0: 9.5.0 was published within 24 hours with zero downloads, so it has had no community exposure. 9.4.0 has had five months and ~5M downloads. Test-only dependency (IsPackable=false) - never ships to consumers.
Mongo2Go ships executable MongoDB binaries to every consumer, which makes this repository a supply-chain target. Documents the rules that follow from that, and makes the two most important ones machine-enforced rather than dependent on a reviewer remembering them. CLAUDE.md: - RULE 1: zero trust for binaries; contributor PRs never introduce bytes under tools/, maintainers re-download with src/MongoDownloader - RULE 2: never trust a contributor-supplied packages.lock.json - RULE 3: audit checklist before adopting any new dependency - RULE 4: no dependency version younger than ~30 days, so a compromised publish has had community exposure before we adopt it - RULE 5: verify, never assert; suspect your own check first CI: - add a pull_request trigger, so fork PRs are actually validated (previously only push was configured, so PR #159 ran no checks at all) - guard-binaries: fails any PR touching tools/ - verify-lockfile: regenerates lockfiles from nuget.org and requires an exact match against the committed ones Both guards were tested against simulated tampering before committing: appending bytes to a vendored mongod is detected, and a committed contentHash alteration is detected. Note: these jobs only block merges once they are configured as required status checks in the branch protection settings.
Adding the pull_request trigger means fork pull requests now execute contributor code in the build job. That is the normal cost of validating pull requests, but the token that code runs alongside should be minimal. The repository default for GITHUB_TOKEN is "write". Set an explicit top-level "contents: read" and re-grant "contents: write" only on the publish job, which needs it to create the GitHub release. Also add an explicit event_name check to the publish gate. It is redundant with the existing ref check, since a pull_request event has ref refs/pull/<n>/merge and cannot match refs/tags/. It is kept because this job spends NUGET_API_KEY, and that is worth making structurally impossible to reach rather than only conditionally unreachable. Verified the gate across event shapes: publish runs only for a pushed version tag, and not for a push to main or for pull requests.
ArchiveExtractor built every destination path with Path.Combine and never checked that the result stayed inside the extraction directory. Archive entry names are attacker-controlled data - they come from a downloaded archive and neither SharpZipLib's ZipFile nor the reported tar entry names sanitise them - so two escapes were possible: - Zip slip: an entry such as "root/../../../../PWNED/bin/mongod" resolved to /PWNED/bin/mongod. Path.Combine neither normalises nor rejects "..". The binary/licence regexes were not a mitigation, because the binary pattern "bin/mongod" is unanchored and a traversal path containing that substring still matches. - Rooted entry names: Path.Combine discards everything before a rooted segment, so a tar entry named "/etc/VICTIM/file" resolved to that real host path and reached extractedFile.Delete(). Both are now resolved with Path.GetFullPath and rejected unless the result is prefixed by the extraction directory. Applied at all three destination constructions: the zip extraction path, and both the delete and move paths in CleanupExtractedFiles. Verified with the payloads that previously succeeded: traversal, rooted entry, and deep traversal are all blocked, while legitimate bin/ and licence entries still resolve unchanged. This does not remove the need for checksum verification. It contains the damage a malicious archive can do; verifying the published sha256 would stop such an archive being opened at all.
MongoBinaryLocator walks upwards from several starting points toward the filesystem root, so the search can reach directories Mongo2Go does not control. A directory planted in any ancestor of a consumer's build output was found before the NuGet cache and executed as mongod. Verified against the published 4.1.0 package: resolution normally lands in the NuGet cache, but a planted tools/mongodb-<os>*/bin in an ancestor takes precedence. Restricting the search would only move the question, because a trusted location can be compromised too. Instead the binaries are now identified by content: MongoBinaries.sha256 records the SHA-256 of every bundled binary and is embedded in the assembly, and a candidate directory is used only if its binaries match. Where they were found stops mattering. A candidate that fails verification is skipped rather than fatal, so an impostor found early cannot mask the real binaries further up. This needed FolderSearch.FindFoldersUpwards, which enumerates every match instead of stopping at the first, including siblings at the same level. Binaries supplied explicitly by the caller, through binariesSearchDirectory or binariesSearchPatternOverride, are used without verification. Pointing Mongo2Go at your own MongoDB is supported and is the existing workaround for a newer server (#132) and for native arm64 (#127); those binaries will legitimately not match. NUGET_PACKAGES stays under verification, since an environment variable is not caller intent. Cost is about 40 ms per directory, cached per process. A missing manifest fails open, since that is a packaging fault of ours rather than evidence against the user's binaries. CI gains verify-binary-manifest, which regenerates the manifest from tools/ and requires an exact match. It runs on push as well as pull_request, because maintainers update binaries by pushing directly and that is exactly when the manifest must be regenerated with them. Verified: a planted directory is rejected and the search continues to the genuine binaries; an explicit override to that same directory is accepted; all 49 tests pass.
…ntly Skipping a directory that fails checksum verification and continuing the search is what makes the search order harmless, but on its own it meant a successful recovery left no trace: a planted directory would be routed around and nobody would ever know it was there. Permissive control flow and quiet control flow are separate choices, and only the first one is wanted. Every rejected directory is now reported through the ILogger already accepted by MongoDbRunner.Start, naming the directory and explaining how to use it deliberately if it is the caller's own build. There is no reason to be discreet about it - a checksum cannot be forged, so nothing is disclosed by saying which directory was ignored. The existing two-argument MongoBinaryLocator constructor is kept so this stays binary compatible.
The downloader is the project's provenance mechanism, but it verified nothing: DataModel parsed only the archive url and discarded the sha1 and sha256 that MongoDB publishes beside it. The binaries committed to tools/ therefore attested to what we happened to receive, not to what MongoDB released. Archive.Sha256 is now parsed and checked after download and before a single entry is read. A mismatch deletes the downloaded file and aborts, rather than warning: these bytes are committed, packaged, and then executed on every consumer's machine, so continuing is never the better option. A missing checksum is also fatal, since an archive whose integrity cannot be established is exactly the case this is meant to catch. ZIP archives were previously read directly over HTTP with range requests and never held in full, which left nothing to hash. Both formats now download to a local file first and extract from it, so verification happens before extraction for every platform. This also removes the reason for the Espresso3389.HttpStream dependency, which is dropped: it built its own HttpClient outside this tool's configuration and used a constructor its own author has deprecated. One less dependency in the tool whose job is trust, and the build is now warning-free. DownloadArchiveAsync switches from OpenWrite to FileMode.Create. OpenWrite does not truncate, so a shorter re-download would keep trailing bytes from a longer previous one and fail the checksum for a misleading reason. Verified against a real release: the published sha256 for the 100.14.0 macOS tools archive matches the downloaded bytes exactly, and flipping a single byte changes the digest completely.
MongoBinaries.sha256 was produced by hand, which left the weakest link in the chain dependent on somebody remembering. It is now written by MongoDownloader as the final step of a download, after extraction and stripping, so it always describes the binaries exactly as they will be committed and packaged. --write-manifest regenerates it from the binaries already in tools/ without downloading anything, so a manifest can be repaired without re-fetching several hundred megabytes. The manifest now maps a file name to a SET of accepted checksums rather than a single one. Options requests both x64 and arm64 for Linux, so a download produces two linux directories, each with its own mongod, and both are legitimately ours. The question the runtime asks is "is this one of the binaries we shipped?", which a set answers and a single value cannot. (The related issue that tools/mongodb-linux* matches both architectures and picks alphabetically is not addressed here.) The CI check now compares sets rather than regenerating the file and diffing text. Reordering entries or editing the comment header no longer fails the build, while adding, removing or altering any entry still does, and the failure names the drift in both directions and gives the command to fix it. Verified: the generated manifest carries the same nine checksums as the hand-written one, the CI check passes against it, and changing a binary without regenerating the manifest fails with a readable diff.
The supply-chain work has no maintainer-facing documentation, which matters most for the parts that only surface when something goes wrong: the CI guards, and the fact that tools/ and MongoBinaries.sha256 must change in the same commit. Covers how to update the bundled binaries, the four verified links between MongoDB's release and what actually executes on a consumer's machine, why the binary search can look in many places without that being a risk, and what each CI guard blocks. Adds the dependency-age and lockfile rules to the maintainer best practices, and notes that pull requests are now validated. Also records two things that are easy to trip over: the downloader always fetches the latest production release and so cannot currently reproduce an older version, and it resolves tools/ by walking up from the current working directory. Every symbol, job name and command referenced was checked against the source rather than written from memory.
The binaries were committed on 2021-03-26; llvm-strip support was added on 2021-09-26 and they have never been re-downloaded since, so the stripper has never once run on what ships. Stripping them now takes the package from 120,508,212 to 94,032,061 bytes - 25.2 MiB, 22% - which is headroom for the architectures that #133 has been blocked on. What this removes is only the static symbol table: .symtab and .strtab account for 15,655,720 of the 15,655,738 bytes dropped from mongod. There are no DWARF sections to remove, because MongoDB already publishes production binaries without debug info and ships debug symbols separately. What remains is load-bearing - .text, .dynsym/.dynstr for dynamic linking, .eh_frame/.gcc_except_table for C++ exceptions - so there is no further saving available from stripping. windows/mongod.exe is deliberately NOT stripped. It saved zero bytes (MSVC keeps debug info in separate PDBs) while its checksum changed, and it is the one binary that cannot be executed on a developer machine here. Zero benefit against untestable risk, so it stays byte-identical to MongoDB's published original. Provenance is preserved. Before stripping, all nine binaries were confirmed byte-identical to the archives MongoDB publishes, each archive verified against its published SHA-256 first. Stripping is applied after that verification, so the chain is: MongoDB's checksum, then a deterministic transformation, then our manifest. The manifest now records the strip tool version, and --write-manifest carries it over when regenerating without stripping. Determinism was measured rather than assumed: llvm-strip 14, 15 and 18 each produced identical output from the same input, on both x64 and arm64 hosts. That is an observation and not a guarantee for future releases, which is exactly why the version is recorded - it makes a disagreement diagnosable. All 49 tests pass, including the import/export tests that exercise the stripped Go binaries for real.
Closes the trap that would have made a naive fix for #127 fail silently. MongoBinaryLocator matched tools/mongodb-linux*/bin and had no notion of architecture at all, so once a second Linux directory existed it would pick one alphabetically. Verified on real hardware before this change: on an arm64 machine with mongodb-macos-amd64 and mongodb-macos-arm64 available, it chose amd64. Checksum verification alone cannot fix this, because both directories are legitimately ours and both match. So the manifest now records architecture as well - <platform>/<architecture>/<file> - and resolution runs in two passes: first accepting only binaries native to the machine, then accepting any architecture we ship. The second pass is not a fallback for broken setups, it is the normal path on Apple Silicon. MongoDB 4.4 has no macOS arm64 build, so those users run the x64 binaries under Rosetta 2 today and must keep working. Making verification architecture-exclusive would have broken every one of them. tools/ directories now carry the architecture, matching what MongoDownloader already produces, and gain mongodb-linux-arm64. Both arm64 archives were verified against MongoDB's published SHA-256 before extraction and stripped with the same pinned llvm-18. CI gains ubuntu-24.04-arm. Without it the matrix proved less than it looked: macos-latest is Apple Silicon but runs x64 under Rosetta, so every job passed for a different reason and none would have caught #127. The libssl1.1 step is now architecture-aware, since security.ubuntu.com serves amd64 only and arm64 packages come from ports.ubuntu.com - verified on real arm64 Ubuntu. fail-fast is disabled so one architecture failing does not hide the others. Verified end to end in containers on both architectures: with both linux directories present, an x64 machine selects linux-x64 and an arm64 machine selects linux-arm64. On arm64 Linux the old x64 mongod dies where the new arm64 mongod reports "db version v4.4.4". Package grows 114.9 -> 122.8 MiB, +7.9 MiB for a whole additional architecture, because stripping paid for most of it. That is 49% of the 262,144,000 byte nuget.org limit. All 49 tests pass.
The package version itself is set by the git tag (MinVer) at release time; this commit only updates the human-facing metadata for the 4.2.0 minor: - README changelog entry for 4.2.0 (arm64, dependency updates, smaller package, supply-chain verification), crediting Simon Schmid for PR #159 - csproj Description now states the bundled architectures explicitly (Windows x64, Linux x64 and arm64, macOS x64) - copyright year bumped to 2026
The guard rejected any pull request touching tools/, which blocked the maintainer's own release PRs - exactly the change that legitimately adds or updates binaries. RULE 1 in CLAUDE.md forbids binaries from a *fork* PR: a same-repo branch could only have been pushed by a collaborator, which is the same trust level as the direct push the rule already sanctions. The guard now fires only when the PR head is a different repository (a fork). Same-repo PRs are the maintainer path and are allowed to touch tools/. A binary changed without a matching manifest update is still caught on every push and PR by verify-binary-manifest, so this does not open a hole - it aligns the guard with the documented trust boundary.
…es reproducible Adds --server-version and --tools-version so the downloader can fetch an exact past release instead of only the latest production one. Server and Database Tools have independent version numbers (e.g. 4.4.4 and 100.3.1), so they are pinned separately. When a version is pinned the tool reads the full release feed (full.json) rather than the current-releases feed, which only lists recent versions. This is what makes the maintainer's documented verification step real: re-download an exact version and confirm the bundled binaries reproduce. Verified in a container that pinning to 4.4.4 / 100.3.1 resolves both from the full feeds, downloads, and passes SHA-256 verification. Also makes stripping reproducible against what is committed. llvm-strip rewrites a binary that has no removable symbols - Windows mongod.exe, whose debug info is in a separate PDB - to the same size but different bytes, which needlessly breaks provenance against MongoDB's published binary. BinaryStripper now restores the original whenever stripping does not reduce size, so such a binary stays byte-identical to upstream. Verified with real llvm-strip 18.1.3: windows/mongod.exe strips to the same size and is kept as the original 8e8609ce..., matching the committed manifest. This replaces the manual revert done when the binaries were first stripped, so the whole tools/ directory is now reproducible by re-running the tool. Note: a full single-command end-to-end run (download + verify + strip + manifest for all platforms) could not be completed on the local Docker VM, which repeatedly crashed while deserializing the 49 MB full.json feed under memory pressure. The individual stages were each verified; the crash is a host limitation, not a logic fault.
The downloader loaded the entire release feed into memory with GetFromJsonAsync<Release> and then picked a single version out of the resulting object graph. For the full feed - about 50 MB and more than a thousand versions, each with every platform download - that was wasteful, and it is what fell over on a memory-constrained machine while pinning an older version. MongoReleaseReader now streams the HTTP response and materialises one Version at a time with a Utf8JsonReader over a small, growable buffer, keeping only the version that matches. Peak retained memory for parsing the full feed drops from 46.0 MiB to 0.5 MiB (measured against the real full.json), and the whole tool now peaks at ~97 MB RSS running a full pinned download. Correctness was checked against a full deserialisation as ground truth: the selected version and its complete download set are byte-identical across buffer sizes from 64 bytes (which forces the element-spanning rewind-and- refill path on every version) up to 64 KiB, the latest-production predicate still resolves 8.3.4, and an absent version returns null. Verified end to end with the actual tool, pinned to 4.4.4 / 100.3.1: it runs to completion and the extracted linux/x64 mongod is byte-identical to MongoDB's published original (230e22c6...). The now-unused Release wrapper class is removed.
…d key Replaces the long-lived NUGET_API_KEY secret with GitHub OIDC. The publish job requests a short-lived (1 hour) nuget.org API key at push time via NuGet/login, so there is no stored key to leak or expire - the expiry of the old key was about to block this very release. - adds id-token: write to the publish job (keeps contents: write for the GitHub release) - NuGet/login@v1 exchanges the OIDC token for a temporary API key - dotnet nuget push uses that key instead of secrets.NUGET_API_KEY Requires a Trusted Publishing policy on nuget.org naming this repository and the workflow file continuous-integration.yml, plus a NUGET_USER secret (the nuget.org profile name). The old NUGET_API_KEY secret can then be deleted.
Adds the publish job to a 'nuget-publish' GitHub environment configured with a required reviewer, so a tagged build pauses for a maintainer's approval before anything is pushed to nuget.org - a last human checkpoint, matching the environment gate already used for the npm release of a sibling project. Self-review is intentionally allowed: the project has a single maintainer, who must be able to approve their own release. For the tightest coupling, set the Environment field of the nuget.org Trusted Publishing policy to 'nuget-publish' as well, so the short-lived key is only ever issued to the approved environment.
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.
Mongo2Go 4.2.0
A minor release: resolves the vulnerable-dependency report (#160), adds Linux ARM64 support (#127), reduces package size (#133), and hardens the supply chain around the bundled MongoDB binaries. No breaking API changes.
User-facing
mongod/mongoimport/mongoexportare now bundled, and binary resolution is architecture-aware. Verified on a realubuntu-24.04-armrunner: the old x64 binary fails withExec format error, the new arm64 binary reportsdb version v4.4.4. Apple Silicon continues on the x64 macOS binaries under Rosetta 2 (MongoDB 4.4 has no macOS arm64 build).Supply-chain hardening
The bundled binaries are executed on every consumer's machine, so this release tightens the chain from MongoDB's release to what runs:
MongoDownloadernow verifies every archive against MongoDB's published SHA-256 before extraction (the field existed in the release JSON and was previously discarded).mongod. This also closes a latent architecture-selection bug that would otherwise have shipped x64 to arm64 machines.Espresso3389.HttpStreamdependency (no longer needed after the extraction rework).CI
pull_requesttrigger (fork PRs previously ran no checks).ubuntu-24.04-armto the matrix — without it the matrix passed for the wrong reasons and could not have caught Arm64 support #127.tools/, verify the lockfile against nuget.org, verify the checksum manifest matchestools/.GITHUB_TOKEN; explicit event guard on the publish job.