Skip to content

chore(security): fix reachable x/crypto ssh vulns, scan driver modules in CI - #14

Merged
juicycleff merged 3 commits into
mainfrom
chore/vuln-sftp-crypto
Aug 14, 2026
Merged

chore(security): fix reachable x/crypto ssh vulns, scan driver modules in CI#14
juicycleff merged 3 commits into
mainfrom
chore/vuln-sftp-crypto

Conversation

@juicycleff

@juicycleff juicycleff commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Items 1 and 2 from the Dependabot triage. This is independent of #13 and touches only go.mod, go.sum and CI.

The blind spot

govulncheck resolves imports per module. The security job comes from the shared xraph/workflows/go-ci.yml@v1 and runs at the repository root, so it only ever scanned the root module. The four driver sub-modules, which is where every third-party dependency the project ships actually lives, were never scanned at all.

That's why CI kept reporting two vulnerabilities while Dependabot reported thirty-nine against those same modules. The drivers matrix job built and tested each one but ran no vuln scan.

What turned up

Scanning each module directly found five reachable advisories in golang.org/x/crypto/ssh from sftpdriver. Reachable, not merely present, so these are not call-graph over-approximations.

sftp.go:169: SFTPDriver.Open calls ssh.Dial
sftp.go:160: SFTPDriver.Open calls ssh.ParsePrivateKey
sftp.go:174: SFTPDriver.Open calls sftp.NewClient, which reaches ssh.channel.Write
Advisory Issue
GO-2026-5020 Infinite loop on large channel writes
GO-2026-5019 FIDO/U2F security key physical-interaction bypass
GO-2026-5018 DoS via pathological RSA/DSA parameters
GO-2026-5017 Client can deadlock the server on unexpected responses
GO-2026-5013 Byte-arithmetic underflow, then panic

Bumping x/crypto from v0.37.0 to v0.52.0 clears all five. Reachable non-stdlib findings for sftpdriver go from five to zero. x/sys came along as an indirect bump, and the module's go directive is untouched, since v0.52.0 asks only for Go 1.25.0 and CI already builds with 1.25.7.

The gate, and why it isn't just "fail on findings"

Two deliberate choices, both aimed at keeping the check meaningful instead of ambient noise.

Standard-library findings report but never gate. A stdlib finding tracks the runner's Go patch release, not anything in this repository, so gating on it reds every branch the moment a new Go version ships. That is exactly what's happening to ci / Security right now: go1.25.12, fixed in 1.25.13, and both traces run through io.ReadFull and sync.Once.Do, which merge every implementation in the binary. Worth knowing that the runner reports fifteen stdlib findings per driver where a local Go 1.26.3 shows eight. Gate on those and all four driver jobs stay permanently red.

Only symbol-level findings gate. When a finding's trace stops at module or package level, the dependency is present but the vulnerable code never runs. That's Dependabot's job, not a build gate's.

The allowlist

gcsdriver, s3driver and azuredriver still have reachable findings, all of them indirect through their cloud SDK trees. A naive blocking gate would have turned three of the four driver jobs red on day one and blocked every unrelated PR after that.

Making the gate non-blocking would have made it decorative, so instead the known findings go in .github/govulncheck-allowlist.txt:

gcsdriver GO-2026-6061   # grpc: xDS RBAC + HTTP/2 server. Needs grpc v1.82.1.
gcsdriver GO-2026-4394   # otel/sdk: PATH hijacking, arbitrary code execution.
s3driver  GO-2026-5764   # aws eventstream decoder panic. Needs service/s3 v1.97.3.

The gate blocks anything new from today while the existing backlog stays visible and attributed to a module. Treat that file as a backlog rather than an exemption. Removing a line is how you enforce a fix: bump the dependency, delete the entry, and the job goes green on its own.

sftpdriver is deliberately absent from it. Its findings were fixed, not listed.

Verification

The gate logic was rehearsed locally against all four modules before any of this was committed.

Module stdlib (reported) module (gating) Result
sftpdriver 1 0 pass
gcsdriver 7 5, all allowlisted pass
s3driver 8 1, allowlisted pass
azuredriver 8 3, all allowlisted pass

Pulling GO-2026-6061 back out of the allowlist correctly fails gcsdriver with NEW (gating): GO-2026-6061, which is the check that the gate isn't vacuous. sftpdriver builds, vets and passes go test -race after the bump.

Two things this run exposed

The drivers, extension, bench and lint jobs were skipping whenever ci / Security failed. A reusable workflow reports one conclusion for all of its jobs, so a stdlib finding at the root marked the whole ci job failed and skipped everything declaring needs: ci. The driver sub-modules and the extension module were not being built or tested at all, and a skipped job reports as neither pass nor fail, so the coverage vanished without a red check anywhere. It also left the govulncheck step above permanently unreachable. They run now unless the workflow is actually cancelled, and needs: ci stays for ordering.

The scan step also wrote every informative line to GITHUB_STEP_SUMMARY and nothing to stdout, so reading it back with gh run view --log showed the script echoed and no decisions. It reports to both now.

Not in here

The root security job still gates on stdlib findings, so ci / Security keeps going red on Go patch releases. The same split applied here would fix it, but that config lives in the shared xraph/workflows repo.

Seventy-two of the hundred and eleven alerts belong to the docs/ Next.js site: next, postcss, js-yaml, sharp. Build-only, not shipped, not part of any Go module. They deserve their own dependabot.yml cadence so they stop drowning the alerts that affect shipped code.

The three allowlisted backlogs need grpc v1.71 to v1.82.1, aws-sdk-go-v2/service/s3 v1.79.3 to v1.97.3, plus x/net and x/text. Those are bigger jumps across cloud SDKs and are better done per module against their integration tests.

One note: this branch and #13 both add a section under ## [Unreleased] in CHANGELOG.md, so whichever lands second hits a trivial conflict there.

…s in CI

govulncheck resolves imports per module, so the shared go-ci.yml security
job — which runs at the repository root — never scanned the four driver
sub-modules. Every third-party dependency the project ships lives there, so
they were entirely unscanned while Dependabot reported 39 alerts against
them.

Scanning each module directly found five reachable advisories in
golang.org/x/crypto/ssh from sftpdriver. Reachable, not merely present:
Open calls ssh.Dial, ssh.ParsePrivateKey and sftp.NewClient directly, so
the traces resolve to called symbols rather than being call-graph
over-approximations. Bumped to v0.52.0, which clears all five. The go
directive is unchanged — v0.52.0 needs only Go 1.25.0.

The new CI step gates on reachable findings in third-party modules and
reports standard-library findings without failing. A stdlib finding tracks
the runner's Go patch release rather than anything here, so gating on it
would red every branch whenever a new Go version lands.

gcsdriver, s3driver and azuredriver still have reachable findings, all
indirect. Rather than leave the new gate non-blocking, they are recorded in
.github/govulncheck-allowlist.txt so the gate blocks anything new from
today. That file is a backlog, not an exemption: removing a line is how a
fix gets enforced, and adding one requires a reason.
A reusable workflow reports a single conclusion for all of its jobs, so
when ci / Security failed, the whole `ci` job was marked failed and every
job declaring `needs: ci` skipped: drivers, extension, bench and lint.

The driver sub-modules and the extension module were therefore not being
built or tested at all. A skipped job reports as neither pass nor fail, so
this left no red check anywhere — the run looked healthy while four jobs
did nothing. It also made the govulncheck step added in the previous commit
unreachable, since it lives in the drivers job.

The current trigger is two standard-library advisories describing the
runner's Go patch release, which has no bearing on whether an independent
module compiles or its tests pass.

Keep `needs: ci` for ordering and run these jobs unless the workflow was
actually cancelled. No gate is weakened: each job still reports its own
result, and ci / Security still fails on its own findings.
Every informative line went to GITHUB_STEP_SUMMARY, so a passing scan left
no trace in stdout. Reading the step back with `gh run view --log` showed
the script being echoed and nothing else, which is indistinguishable from a
step that parsed nothing and passed vacuously.

Report both places: the summary for the run page, stdout for anyone
debugging from the CLI or reading a failed job's log.
@juicycleff
juicycleff merged commit 0be8ecc into main Aug 14, 2026
17 of 18 checks passed
@juicycleff
juicycleff deleted the chore/vuln-sftp-crypto branch August 14, 2026 01:38
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.

1 participant