> ## Content Index
> Fetch the complete content index at: https://sudosecurity.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# A 257-Byte Password Is the Key: Inside the OpenSSH musl Bypass
- URL: https://sudosecurity.org/openssh-sshole-cve-2026-123158-musl/
- Published: 2026-11-11T19:08:06.000Z
- Updated: 2026-11-11T19:08:06.000Z
- Description: CVE-2026-123158 lets an attacker log into a locked account on musl-based systems like Alpine by sending a password nobody set. The trigger is one number, 257, and the reason is a C library returning a single asterisk. I reproduced the exact boundary against musl 1.2.4 and read the fix.
- Author: Traven
- Tags: Cybersecurity, Authentication, CVE, Server Security, Containers, Vulnerabilities, Exploits, Networking, Open Source

A locked account is supposed to be a door with no key. You set a user's shadow password field to `*` or `!`, and from then on no string anyone can type hashes to that value. `*` is not a hash. It is a marker that means "nothing goes here." Key-only SSH users, service accounts, the default users baked into a cloud image: all of them sit behind that marker, and the whole security model assumes it cannot be opened with a password.

On musl-based systems, you can open it with a password. Not the right password — there isn't one — any password, as long as it is at least 257 bytes long.

That is `CVE-2026-123158`, nicknamed **sshole**, reported by Özgün Kültekin and fixed in OpenSSH 10.6p2 on 5 November 2026\. MITRE rates it 8.1 High. It affects OpenSSH built against musl — the small C library used by Alpine Linux, and the enormous number of containers built `FROM alpine` — when sshd authenticates passwords itself instead of delegating to PAM. The strange part, and the reason it's worth a full write-up rather than a patch note, is that the bug isn't really in OpenSSH — and OpenSSH is the thing that fixed it.

## What This Article Establishes

Everything below is checked two ways: the fix is read from the OpenSSH source tree at the actual commits, and the root-cause library behaviour is reproduced against musl 1.2.4 on a real machine, with the output printed. No part of the attack against a live sshd is reproduced or described step by step — the mechanism is a C-library return value, and that is all you need to understand your exposure.

## The One-Asterisk Problem

When sshd checks a password without PAM, it ends up in `sys_auth_passwd()` in `auth-passwd.c`. Stripped to the part that matters, the pre-fix code did this:

```c
/* salt is only taken from the stored field when it is 2+ chars */
if (authctxt->valid && pw_password[0] && pw_password[1])
    salt = pw_password;
encrypted_password = xcrypt(password, salt);

return encrypted_password != NULL &&
    strcmp(encrypted_password, pw_password) == 0;

```

Read it as the login decision it is: hash what the user typed, compare it to what is stored, and accept if the two strings are equal. This is correct, and it's been correct for decades. It delegates the hard part — turning a password into a hash — to the C library's `crypt()`, and it trusts that `crypt()` returns either a real hash or something that cannot be mistaken for the stored value.

That trust is the bug. Here is what musl's `crypt()` actually returns as you lengthen the password, reproduced on musl 1.2.4 with a normal `$6$` (SHA-512) salt:

```
  password =    8 bytes -> crypt() = $6$abcdefghijklmnop$l5aaqpnoveU1IYaem...
  password =  255 bytes -> crypt() = $6$abcdefghijklmnop$CH5lkrzAjWikQnLwZ...
  password =  256 bytes -> crypt() = $6$abcdefghijklmnop$PZVtP6.vU8cmlXsyB...
  password =  257 bytes -> crypt() = *
  password =  512 bytes -> crypt() = *
  password = 1024 bytes -> crypt() = *

```

At 256 bytes you get a hash. At 257 bytes you get a single asterisk. musl caps the password it will hash, and on input past the cap it returns the string `"*"` as an error sentinel.

Now put that next to the stored value for a locked account, which is very often exactly `"*"`. The comparison `strcmp(encrypted_password, pw_password)` becomes `strcmp("*", "*")`, which is `0`, which means equal, which means **authenticated**. The attacker supplied a 257-byte string of garbage. The library turned it into `*`. The account's stored secret was `*`. They match, and sshd opens the door.

The boundary is not approximate. I walked it one byte at a time:

```
256 -> full hash
257 -> "*" (locked-token match)

```

256 is safe, 257 is the key. That lines up exactly with the public description of passwords "between 256 and 1024 characters" working — 1024 being OpenSSH's own `MAX_PASSWORD_LEN`, the longest password it will forward to the library at all.

## Why glibc Shrugs and musl Falls Over

This is the detail that decides whether you are affected, so it is worth seeing rather than taking on faith. The same program, same salt, same passwords, built against glibc instead of musl:

| Password length | musl 1.2.4 returns | glibc returns |
| --------------- | ------------------ | ------------- |
| 256 bytes       | full $6$ hash      | full $6$ hash |
| 257 bytes       | \*                 | full $6$ hash |
| 512 bytes       | \*                 | \*0           |
| 1024 bytes      | \*                 | \*0           |

Two differences, and both protect glibc. It tolerates longer passwords before giving up, and when it does give up it returns `"*0"` — two characters — not `"*"`. A locked account stores `"*"`, and `"*0"` is not equal to `"*"`, so the comparison fails and the door stays shut. glibc is not safer here because it is more careful about passwords; it is safer because its error sentinel is one character longer than musl's and happens not to collide with the locked-account marker.

That is the whole difference between a system you can walk into and one you cannot: one byte in an error string, chosen years ago by two different libc authors with no idea it would ever be compared against `/etc/shadow` by a third program.

> **We recommend reading** [What Is Docker?](https://sudosecurity.org/what-is-docker/) to continue reading our selection of content. The blast radius of this bug is containers: every image built `FROM alpine` with an SSH server inherited the vulnerable OpenSSH, which is why a libc quirk became everyone's problem.

[What Is Docker?](https://sudosecurity.org/what-is-docker/) How container images are layered, and why a flaw in a base image propagates to everything built on top of it. SudoSecurity

## The Exploit Conditions, Stated Plainly

A bug is only as serious as how often its preconditions are all true at once. Here they are, and for sshole they line up uncomfortably well on exactly the systems people treat as minimal and therefore safe:

1. **sshd authenticates passwords itself, not through PAM.** On Alpine this is the default. The `openssh-server` package has no PAM; `openssh-server-pam` does, but plenty of installs use the former, and `UsePAM no` reintroduces the path even where PAM exists.
2. **Password authentication is enabled.** Also the default.
3. **The target account's shadow field is `*`** (or another short locked token the comparison can match). Key-only users, service accounts and several cloud-image users are stored this way.
4. **At least one account on the box has a real `$5$` or `$6$` hash.** This is the non-obvious one. sshd's salt-picking helper walks the shadow file to choose a salt type; it needs to land on a modern SHA-crypt entry so that `crypt()` takes the SHA path, which is the path with the 256-byte cap. Pure-DES systems behave differently.
5. **The attacker can reach the SSH port.**

Note what is *not* required: a username with a password, any credential, any prior access. The attacker needs a reachable port and the name of a locked account, and locked account names are rarely secret — `git`, `postgres`, a service user, the default image user.

Root is partially shielded by `PermitRootLogin prohibit-password`, which refuses password logins for root regardless. But Alpine's tiny-cloud images have shipped a passwordless `alpine` user that can escalate with `doas`, and any non-root locked account that a human can `su` or `sudo` from is a foothold. "It's only a locked service account" is not the comfort it sounds like when the service account can run the service.

## Reading the Fix Out of the Tree

OpenSSH's response is unusually pointed, and the commit messages are worth quoting because they tell you where the maintainers think the fault lies. The fix is commit `4353fdc`, "avoid password authn bypass," on 5 November, and Damien Miller wrote:

> This bypass affects some configurations and runtime configurations. We do not consider the root cause to be within OpenSSH but we are well equipped to avoid it.

That is a maintainer saying, politely, "this is musl's `crypt()` returning a matchable sentinel, not our bug — but we are the layer that can stop it, so we will." The change adds a minimum-length floor on the stored hash:

```c
#define MIN_PASSWORD_HASH_LEN   4

ret = encrypted_password != NULL &&
    strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
    strcmp(encrypted_password, pw_password) == 0;

```

A real crypt hash is never shorter than a handful of characters; `$6$` hashes run to 80-plus. A locked token like `*`, `!`, `x` or `*LK*` is one to four characters. By refusing to accept any match where the *stored* value is shorter than four bytes, the comparison can no longer be satisfied by a one-character sentinel, whatever the library returned. `"*"` has length 1, which is less than 4, so the match is rejected before `strcmp` even gets a vote.

There is a second commit, `1d28115`, "harden password checking," from 9 November, four days later:

> Never allow an account that we couldn't find a salt for to pass password authentication, even if crypt() returns hash that matches what is in shadow.

```c
ret = encrypted_password != NULL && salt != NULL &&
    strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
    strcmp(encrypted_password, pw_password) == 0;

```

The added `salt != NULL` closes the sibling path: the case where the stored field is so short (one character or empty) that sshd never derived a salt from it and fell back to picking one from another account. Belt, then braces. The same two edits were applied to the UnixWare `port-uw.c` path, so this is not Alpine-specific defensive code — it is OpenSSH hardening the comparison everywhere it does its own password check.

The fix also quietly corrects a memory-ownership bug in passing: `shadow_pw()` now returns a freshly allocated copy that the caller frees with `freezero()`, rather than a pointer into library-owned storage. That is not part of the CVE, but it shipped in the same commit, and it is the kind of cleanup that tends to ride along with a security fix under deadline.

> **We recommend reading** [npm Dependency Security Before CI: Where Trust Decisions Actually Happen](https://sudosecurity.org/npm-dependency-security-before-ci-trust-decisions-2026/) to continue reading our selection of content. sshole is the same lesson one layer down: a dependency you did not write — here, the C library — made a decision your security depended on, and you never saw it.

[npm Dependency Security Before CI: Where Trust Decisions Actually Happen](https://sudosecurity.org/npm-dependency-security-before-ci-trust-decisions-2026/) Why the trust boundary in a modern build is the dependency you inherited, not the code you wrote. SudoSecurity

## What To Actually Do

Patch first, reason later. OpenSSH 10.6p2 is the upstream fix, released within a day of the report. On Alpine, the fix was backported to every supported branch rather than only edge — if you are on a maintained Alpine release, `apk upgrade openssh-server` (or `openssh-server-pam`) pulls a fixed build without a version jump. The branch-specific fixed versions run from `9.0_p1-r6` on Alpine 3.16 up to `10.6_p2-r0` on edge, so do not look for a single version number; look for the `-rN` bump on your branch.

If you cannot patch immediately, the mitigations are the preconditions in reverse:

- **Switch sshd to PAM.** `openssh-server-pam` with `UsePAM yes` moves the password check out of the vulnerable path entirely, because PAM does the hashing and never performs this comparison. This is the strongest single mitigation.
- **Disable password authentication.** `PasswordAuthentication no` with key-only access removes the attack surface. On a key-only server this costs you nothing, and it is where most hardened SSH configs already are.
- **Audit your locked accounts.** Anything with `*` or `!` in its shadow field is a potential target on an unpatched musl box. You cannot change libc's sentinel, but you can make sure no locked account is reachable and privileged.

For containers specifically: this is a base-image problem, which means it is a rebuild problem, not just a running-container problem. An image built `FROM alpine` with an SSH server inherited the vulnerable OpenSSH, and every layer on top carries it. Rebuild from a patched base and redeploy; patching a running container is a temporary measure that your next `docker build` will undo.

## What This Doesn't Cover

I did not reproduce the end-to-end attack against a live sshd, and this article deliberately gives no payload, no exploit script and no step-by-step. The reproduction here is a single library call — `crypt()` with a long password — printed to show the sentinel it returns. That is the root cause and the thing that decides your exposure; the rest is a string comparison you can read in the source. There is a public proof-of-concept in circulation, which is a reason to patch faster, not a reason for me to publish another one.

I have not audited every short locked-token value against every musl crypt path. I demonstrated the `"*"` case because it is the common one and the one the reporting centres on; `!` and other tokens behave differently and the fix's length floor covers them as a class rather than one at a time. I also have not tested musl versions other than 1.2.4, though the 256-byte input cap is longstanding musl behaviour rather than a recent change.

Out of scope entirely: the OpenSSH key-exchange and protocol surface, PAM's own configuration pitfalls, and the general question of whether minimal container images trade too much security for size. That last one is a real argument with defensible answers on both sides, and it is bigger than one CVE.

## The Actual Point

sshole is a collision between three correct components. musl's `crypt()` returns `"*"` on overlong input, which is a reasonable sentinel. A locked account stores `"*"`, which is the conventional marker. OpenSSH compares the two with `strcmp`, which is the obvious way to check a password. Each decision's defensible in isolation. Put them in a line and they spell "log in with no password."

That is the uncomfortable shape of a lot of modern security bugs, and it's why the fix lives in OpenSSH even though Damien Miller is right that the root cause isn't OpenSSH's. Nobody owned the interaction. The library author did not know what the sentinel would be compared against. The distro that set the locked token did not know the library would emit a matching one. The application in the middle trusted its dependency to return something unmatchable, which is a reasonable thing to trust, and was true for decades on the libc its authors tested against.

The practical lesson is the one that keeps recurring on this site: your security depends on code you did not write and interactions nobody tested — the same [supply-chain](https://sudosecurity.org/what-is-supply-chain-security/) reality we keep coming back to — and the C library under a container is as much your attack surface as anything in your own repository. The fix was one line — a length check — and the hard part was never writing it. The hard part was noticing that a password could become an asterisk.

*The fix diffs above are reproduced from the OpenSSH portable tree at commits `4353fdc` and `1d28115`. The musl and glibc `crypt()` outputs are real program output from musl 1.2.4 and glibc 2.39 on x86-64, not reconstructed from description.*