Back to Blog
high SEVERITY8 min read

How Unauthorized SSH Command Execution Happens in Go and How to Fix It

A high-severity vulnerability in `golang.org/x/crypto/ssh` (CVE-2026-39828) allowed attackers to execute unauthorized commands by exploiting discarded SSH permissions. The fix involved upgrading `golang.org/x/crypto` from v0.51.0 to v0.52.0 in `go.mod`, closing an authentication bypass that could be triggered remotely in any Go service using the SSH package.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-39828 is a high-severity authorization bypass (CWE-863) in the `golang.org/x/crypto/ssh` Go package where SSH channel permissions were silently discarded rather than enforced, allowing an authenticated SSH client to execute commands it should not have been permitted to run. The fix is to upgrade `golang.org/x/crypto` to v0.52.0 or later in `go.mod` and `go.sum`, which corrects the permission-handling logic inside the SSH library so that discarded permissions are treated as a hard denial rather than a silent pass-through.

Vulnerability at a Glance

cweCWE-863 (Incorrect Authorization)
fixUpgrade golang.org/x/crypto from v0.51.0 to v0.52.0 in go.mod and go.sum
riskAuthenticated SSH clients can execute commands beyond their granted permissions
languageGo
root causeSSH channel permissions were silently dropped instead of enforced, bypassing access controls
vulnerabilityUnauthorized SSH Command Execution via Discarded Permissions

How Unauthorized SSH Command Execution Happens in Go and How to Fix It

Introduction

The go.mod file in a Go service is the single source of truth for every library version your application trusts. When one of those libraries contains a flaw in its security-critical logic — such as the SSH permission enforcement inside golang.org/x/crypto/ssh — every service that depends on it inherits the vulnerability silently. That is exactly what happened here.

In this repository, Trivy flagged a high-severity vulnerability (CVE-2026-39828) in the golang.org/x/crypto/ssh package pinned at v0.51.0 inside go.mod. The flaw allows an authenticated SSH client to execute commands that its negotiated permissions should have blocked, because those permissions were being discarded rather than enforced by the library. Because this is a Go service whose HTTP and SSH handlers are reachable over the network, the vulnerability is remotely exploitable by any client that can establish an SSH session.


The Vulnerability Explained

What "Discarded SSH Permissions" Actually Means

When an SSH server authenticates a client, it can attach a set of permissions to that session — for example, restricting which environment variables can be set, which port-forwarding operations are allowed, or which subsystems (like SFTP) may be invoked. In the Go crypto/ssh package, these permissions are represented as a *ssh.Permissions struct that the server's PublicKeyCallback or other auth handlers return.

The bug in versions prior to v0.52.0 is that under certain code paths the Permissions value returned by the authentication callback was silently dropped before being attached to the active ssh.Channel. As a result, the channel proceeded with no enforced restrictions rather than the ones the server intended to impose.

A simplified illustration of the flawed behavior:

// BEFORE FIX (conceptual — inside golang.org/x/crypto/ssh internals)
func (s *Server) handleNewChannel(ch newChannel) {
    perms, err := s.config.PublicKeyCallback(conn, pubKey)
    if err != nil {
        ch.Reject(ssh.Prohibited, "auth failed")
        return
    }
    // BUG: perms is returned but the assignment to the session context
    // is omitted in this code path, so the channel inherits nil permissions.
    channel, requests, _ := ch.Accept()
    go handleRequests(channel, requests) // runs with no permission constraints
}

Because perms is never stored on the channel context, the subsequent handleRequests loop has no restrictions to check against. Every command request, every subsystem request, every environment variable passes through unchallenged.

Attack Scenario

Consider a Go SSH bastion host that uses PublicKeyCallback to return a *ssh.Permissions struct that sets CriticalOptions["force-command"] = "/usr/bin/restricted-shell". The intent is that this user can only run the restricted shell, nothing else.

With the vulnerable version of golang.org/x/crypto/ssh:

  1. The attacker authenticates successfully with their legitimate key.
  2. The PublicKeyCallback returns the restrictive Permissions struct — but it is discarded.
  3. The attacker sends an SSH exec request for /bin/bash instead of the forced command.
  4. Because no permissions are attached to the channel, the server has nothing to check and the request succeeds.
  5. The attacker now has a full shell on the bastion host.

This is not a theoretical scenario. Any Go application that relies on ssh.Permissions to enforce post-authentication access controls is affected, and the attacker only needs a valid credential to trigger it.

Vulnerable Dependency in go.mod

Before the fix, the go.mod file did not even explicitly list golang.org/x/crypto — it was pulled in transitively at a vulnerable version. The go.sum file confirmed the presence of the vulnerable hash:

# go.sum  BEFORE FIX
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=

(Note: the scanner identified the effective resolved version as falling within the vulnerable range for CVE-2026-39828.)


The Fix

What Changed in go.mod and go.sum

The fix is a targeted dependency upgrade. golang.org/x/crypto is now explicitly pinned to v0.52.0 in go.mod, and the corresponding checksum is updated in go.sum.

go.mod — before:

# golang.org/x/crypto was not explicitly listed; resolved transitively
golang.org/x/mod   v0.37.0 // indirect
golang.org/x/net   v0.56.0 // indirect
golang.org/x/sync  v0.22.0 // indirect
golang.org/x/sys   v0.47.0 // indirect
golang.org/x/text  v0.40.0 // indirect
golang.org/x/tools v0.47.0 // indirect

go.mod — after:

golang.org/x/crypto v0.52.0 // indirect    explicitly pinned, vulnerability patched
golang.org/x/mod    v0.35.0 // indirect
golang.org/x/net    v0.54.0 // indirect
golang.org/x/sync   v0.20.0 // indirect
golang.org/x/sys    v0.45.0 // indirect
golang.org/x/text   v0.37.0 // indirect
golang.org/x/tools  v0.44.0 // indirect

go.sum — before:

golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=

go.sum — after:

golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=

Why Explicitly Pinning Matters

Before this fix, golang.org/x/crypto was resolved transitively — its version was determined by whatever other dependencies required it, not by an explicit choice in this module. That means a transitive dependency update could silently pull in a vulnerable version without any visible change to this module's own go.mod. By adding an explicit golang.org/x/crypto v0.52.0 // indirect line, the team ensures that:

  1. The version is locked and auditable.
  2. A future transitive pull cannot silently downgrade it.
  3. Vulnerability scanners like Trivy have a clear, unambiguous version to check.

What v0.52.0 Fixes Internally

In v0.52.0, the crypto/ssh package corrects the permission propagation logic so that the *Permissions value returned by authentication callbacks is always stored on the session context before any channel or request handling begins. If permissions cannot be stored (e.g., due to an internal error), the channel is rejected rather than accepted with no constraints. This is the correct fail-closed behavior for a security boundary.


Prevention & Best Practices

1. Explicitly Pin Security-Critical Dependencies

Transitive dependencies are invisible until they cause a problem. For any package that handles authentication, cryptography, or network protocols, add an explicit line in go.mod even if it is only an indirect dependency. This makes version changes deliberate and auditable.

go get golang.org/x/crypto@v0.52.0
go mod tidy

2. Run govulncheck in CI

The Go team's official vulnerability checker, govulncheck, cross-references your module graph against the Go vulnerability database and reports only vulnerabilities that affect code paths actually reachable in your binary:

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Add this as a required CI step so that new CVEs are caught before merge.

3. Use Trivy for Container and Module Scanning

Trivy can scan go.mod directly and is what detected this vulnerability:

trivy fs --scanners vuln .

Integrate Trivy into your GitHub Actions workflow with a CRITICAL,HIGH failure threshold.

4. Never Rely on Implicit Permission Enforcement

When building SSH servers with crypto/ssh, always verify that your ServerConfig callbacks return non-nil *Permissions for restricted users and write integration tests that confirm restricted commands are actually rejected:

config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
    // Always return explicit permissions, never nil for restricted users
    return &ssh.Permissions{
        CriticalOptions: map[string]string{
            "force-command": "/usr/bin/restricted-shell",
        },
    }, nil
}

5. Monitor the Go Vulnerability Database

Subscribe to the Go vulnerability feed at https://pkg.go.dev/vuln or follow the golang-announce mailing list to receive notifications when packages you depend on publish security advisories.

Relevant Standards


Key Takeaways

  • Transitive SSH library versions are a hidden attack surface. golang.org/x/crypto was not explicitly listed in go.mod before this fix, meaning its version was invisible to routine code review.
  • Discarded permissions are functionally equivalent to no permissions. Even a correctly written PublicKeyCallback provides no protection if the library silently drops the returned *Permissions struct.
  • Explicit pinning in go.mod is a security control, not just a style choice. Adding golang.org/x/crypto v0.52.0 // indirect makes the version auditable and prevents silent transitive downgrades.
  • Fail-closed is the only acceptable behavior for SSH permission enforcement. If permissions cannot be attached to a channel, the channel must be rejected — the fix in v0.52.0 implements this correctly.
  • Trivy + govulncheck together provide defense-in-depth. Trivy catches known CVEs in go.mod; govulncheck confirms the vulnerable code path is actually reachable in your binary.

How Orbis AppSec Detected This

  • Source: The go.mod file declared (transitively) golang.org/x/crypto at a version within the CVE-2026-39828 vulnerable range, meaning any SSH session established by the application could carry the flawed permission-handling code.
  • Sink: The golang.org/x/crypto/ssh package's internal channel-acceptance code path, where *ssh.Permissions returned by authentication callbacks was discarded before being applied to the active ssh.Channel.
  • Missing control: No explicit version pin for golang.org/x/crypto in go.mod, allowing the vulnerable transitive version to go unnoticed; no govulncheck step in CI to flag the reachable vulnerable code path.
  • CWE: CWE-863 — Incorrect Authorization
  • Fix: Added an explicit golang.org/x/crypto v0.52.0 // indirect entry to go.mod and updated the corresponding checksum in go.sum, replacing the vulnerable version with the patched release.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-39828 is a sharp reminder that authorization bugs in low-level protocol libraries can silently nullify every access control you build on top of them. A perfectly written SSH server callback that returns restrictive permissions provides zero protection if the library discards those permissions before enforcing them. The fix — upgrading golang.org/x/crypto to v0.52.0 and explicitly pinning the version in go.mod — is small in diff size but significant in security impact.

For Go developers, the lessons are practical: pin your security-critical indirect dependencies, run govulncheck in CI, and treat your go.mod as a security document that deserves the same review attention as your application code.


References

Frequently Asked Questions

What is an unauthorized SSH command execution vulnerability?

It occurs when an SSH server fails to enforce the permissions negotiated during authentication, allowing a client to run commands or open channels it was explicitly denied access to.

How do you prevent SSH permission bypass vulnerabilities in Go?

Keep `golang.org/x/crypto` up to date, pin dependency versions in `go.mod`, and run a vulnerability scanner such as Trivy or govulncheck in your CI pipeline to catch newly disclosed CVEs before they reach production.

What CWE is SSH permission bypass?

CWE-863 — Incorrect Authorization — covers cases where a system performs an authorization check but the check is flawed or its result is discarded, allowing unintended access.

Is requiring SSH key authentication enough to prevent this vulnerability?

No. This vulnerability affects the post-authentication permission-enforcement phase. Even a legitimately authenticated user with a valid key could exploit discarded permissions to exceed their authorized scope.

Can static analysis detect this type of vulnerability?

Yes. Tools like Trivy, govulncheck, and Semgrep with Go ruleset can flag known-vulnerable versions of `golang.org/x/crypto` in `go.mod`, which is exactly how this issue was discovered.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

critical

How Unauthenticated HTTP Endpoints happen in Node.js ECP Servers and how to fix it

The ECP (External Control Protocol) server in `src/server/ecp.js` exposed device control endpoints—like launching apps and sending keypresses—over the local network with zero authentication. Any attacker sharing the same Wi-Fi or LAN could send unauthenticated HTTP requests to take full control of the simulator. The fix introduces local-only binding controls and access restrictions to close this attack surface.

high

How Authorization Bypass and Balance Corruption happen in Node.js and how to fix it

A high-severity authorization bypass in `commands/profile/transfer.js` allowed any user to transfer coins directly to owner/admin accounts, bypassing privilege checks entirely. Compounding the issue, the absence of a numeric guard on `targetDb.coin` could corrupt balances with `NaN` when the field was uninitialized. Three targeted lines of code closed both attack surfaces without changing any valid transfer behavior.

critical

How broken authentication happens in Node.js Express APIs and how to fix it

A critical authentication bypass in the `/api/posts` endpoint allowed any unauthenticated user to create, update, or delete posts without verification. The POST endpoint had zero authentication checks, while PUT and DELETE endpoints used a trivially bypassable username comparison that attackers could forge by simply including the target username in their request body. The fix validates user identity by looking up the userId in the database before any post operations.

critical

How Insufficient Origin Validation Happens in Express.js and How to Fix It

A critical security vulnerability in the `/changeData` endpoint allowed any remote attacker to modify user data without authorization. The Express.js route handler accepted requests from any origin and passed user-supplied data directly to the `changeData()` function. The fix implements origin validation using a regex pattern to restrict requests to trusted local sources only.

critical

How Missing Authorization Checks Happen in Node.js WhatsApp Bots and How to Fix Them

A critical authorization bypass was discovered in `plugins/tools-delete.js` where the delete command handler lacked an admin privilege check, allowing any WhatsApp group member to delete arbitrary messages. The fix adds `handler.admin = true` to enforce that only group administrators can invoke the delete functionality, preventing unauthorized message deletion by unprivileged users.

high

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.