Top
Best
New

Posted by josemrb 5 hours ago

sshole(sshhole.com)
1389 points | 371 comments
tonnydourado 2 hours ago|
Am I missing something or is this written up to seem a lot worse than it actually is?

It starts with a bold claim: "ssh accounts that should not accept a password will accept any password between 256 and 1024 characters". But then it keeps adding qualifiers:

- sshd compiled with musl

- not using PAM

- password logins enabled

- at least one account with '*' as the password "hash"

- at least one OTHER account with a valid password

- server is aligned with 2 ley lines

- hacker is wearing a blue t-shirt and green socks

Ok, the last two are an exaggeration, but still. The conditions are all ANDed, miss a single one and you're fine.

On top of that, as someone else in these comments reminded, the target account needs to have a valid login shell or other exploitable program assigned to it, and these passwordless accounts tend to have nologin or something similar.

To me this seems to fall on the "technically possible, practically very unlikely" bucket, kinda like the PHP vulnerability from the other day. Maybe this has gone unnoticed for so long because it just doesn't happen.

pizlonator 2 hours ago||
There are a lot of systems that meet those requirements.

> - sshd compiled with musl

So alpine as well as a bunch of other embedded Linuxes. Huge install base.

> - not using PAM

What you get if you try to have a minimal setup. Default sshd config if you install from source.

> - password logins enabled

The default. Super common.

> - at least one account with '*' as the password "hash"

Almost every Linux system.

> - at least one OTHER account with a valid password

Almost every Linux system.

So sure, there are caveats, but they cover a massive install base.

This is a super bad bug

rhet0rica 1 hour ago|||
Updates since this was posted:

- blue t-shirts and green socks have been added to the default loadout for new hackers, since it's optimal according to AI and costs nothing to the attacker in terms of compute

- there are so many ley line "maps" out there that basically any server placement counts as aligned with at least two, so this one's free also

Onavo 13 minutes ago||
Blue t shirts and green socks?
tonnydourado 2 hours ago||||
How many alpine installs are running sshd? Most containers using it as a base image aren't. Embedded Linux is unlikely to be running it, too. And if one IS running sshd, how often is it with unchanged defaults? I don't buy the inference here.

I'll concede that embedded linux might be my blindspot here. Maybe cheap web hosting, too, but that seems much less likely to be using musl.

svnt 2 hours ago|||
Embedded linux is your blindspot here.
__MatrixMan__ 1 hour ago|||
Rumor has it that Cisco has been hurriedly patching something and refusing to tell their customers what it is. I'm thinking it's this.
tchebb 1 hour ago||||
Most embedded systems I've seen use glibc or uClibc. I agree it would make sense for them to use musl, but the embedded space moves very slowly.
pizlonator 2 hours ago|||
Yes
andix 2 hours ago|||
A lot of embedded systems use musl and no PAM. Some of them might use Alpine, but the distribution doesn't matter that much.
3m4r 1 hour ago|||
Most of those would be using dropbear rather than openssh.
tonnydourado 2 hours ago|||
And ssh? I'd assume most would use proprietary or industry-specific standard for data IO, or just https, for domestic IoT stuff
andix 1 hour ago||
And also ssh in many cases.
pera 1 hour ago||
I thought Dropbear was pretty much the defacto SSH server for embedded applications, is OpenSSH+musl actually a common combination for these scenarios?
utopiah 1 hour ago||||
I'm genuinely curious what's the Venn diagram here because I feel a lot of embedded systems precisely have one condition but not the other, e.g. password yes but not necessarily multiple accounts, e.g root only, while using musl. Namely almost all for almost all, but if only one is not overlapping another then it's actually very few.
yjftsjthsd-h 56 minutes ago||||
>> - at least one account with '*' as the password "hash"

> Almost every Linux system.

But not, notably, on Alpine, which uses '!' instead.

Edit: or maybe yes on Alpine? I checked my desktop and it has zero *s in shadow, but Alpine's post seems to say they do use * sometimes

tremon 24 minutes ago||
I thought they had a difference in meaning, i.e. * means the user has no password and ! means that password logins are disabled for this user, but the man page does not make it clear if there's a distinction.

From man 5 shadow:

> A password field which starts with an exclamation mark means that the password is locked.

> If the password field contains some string that is not a valid result of crypt(3), for instance ! or *, the user will not be able to use a unix password to log in (but the user may log in the system by other means).

qwm 1 hour ago||||
For what it's worth, default Alpine installs don't have an account with * as its password hash if you use the root account as your main one. Not sure if that's also true for ones where you set up a user, maybe those ones are vulnerable.
alexchamberlain 34 minutes ago||||
> password logins enabled

Disabling sshd password logins is security 101, and the default for some cloud provided images these days. If password logins haven't been disabled, what other doors have been left open anyway?

LinaLauneBaer 41 minutes ago||||
does this really matter? It could be used to do something devastating. The number of systems that match those requirements is only one factor. If just a handful of those are highly critical for humanity it could lead to disaster. Eg. hacks into fridges, centrifuges etc.
vadansky 2 hours ago|||
> > - password logins enabled

> The default. Super common.

This always seems bonkers to me. I think SSHD should give you ONE password free connection of setup to copy your key over and then it just shuts the door behind it.

diath 6 minutes ago|||
Not sure why you're getting downvoted as from security standpoint that would be the right thing to do; but actual good server providers take it up a notch and allow you to upload the public key through their admin panel during server set up and the password you get only works locally for sudo etc. There's no reason why a fresh Linux setup should ever offer a publicly available SSH server with password based auth out of the box.
hnfong 1 hour ago|||
I think it has to be a time limited FREE offer. (like, seriously, what if the user doesn't take the offer?)

It might actually a good idea, but I would likely avoid it since my built-in Bayesian filter would just mark it as spam.

hnfong 2 hours ago|||
Alpine Linux (which uses musl) images are popularly deployed by those who use docker, and apparently the issue is there on the default install.

Not sure whether the PHP vulnerability actually had been exploited, but I'm pretty sure there are literally thousands of machines out there affected by this ssh issue right now as we speak.

This isn't going to end well.

---

Addendum (which may or may not be related to the above) --

Everyone has heard of the tale of the boy who cried wolf. The main moral of the story is not to troll your fellow townsfolks so they don't distrust you. But everyone with a sane mind gets that.

The (IMHO) under-appreciated lesson is actually: don't be those villagers. Believe stories you are given, even if you might look like a fool for believing in them. Don't assume what happens is the same as what happened in the past. Look at the facts yourself. Trust and verify.

VorpalWay 2 hours ago|||
You don't typically run sshd in containers, instead you only run whatever single service you set up your container for.

Sure there are native Alpine installs too, but that is far less common of a use case.

tonnydourado 2 hours ago||||
Who's out there running sshd inside a docker container? It's almost MORE work than exec-ing into the container.
zbentley 14 minutes ago|||
Several CICD solutions which offer "shell into container to debug a build failure".

You'd think they'd all do the smart thing and exec a shell into the container's namespace. You'd be wrong.

cluckindan 1 hour ago||||
Probably a lot of companies which have their own k8s cluster and segregate kubectl access to operations only. Developers still need a way to their app backend in some cases.
chuckadams 57 minutes ago||
Kubernetes already comes with better solutions for that sort of thing. I could see a ssh bastion container for bridging to legacy infrastructure though. Manage its keys right and it's probably more secure than a lot of alternatives.
Jolter 1 hour ago||||
I have done that in the past, in order to be able to automate copying files to the container during testing or data migration. It was an ugly hack and now I need to make double sure none of those containers are still running…
Brian_K_White 47 minutes ago|||
Everyone who has access to their container and not the host except by some terrible vnc or web console.
andix 2 hours ago||||
Alpine in containers is usually not affected. Most images/containers haven't ssh/sshd installed at all. Remote access is usually done from the host system.
63stack 1 hour ago|||
You could start with trust and verifying what you wrote first, the popular usage of alpine in docker containers don't run sshd.
huslage 12 minutes ago|||
It’s irrelevant how widespread it is. It’s a bad bug that should be fixed. Why do people always say stuff like this? If you’re running a system that is exploitable then you deserve to know about it and to be provided a fix. It doesn’t matter all that much how widespread it is. The CVE system doesn’t exist to cater only to highly exposed systems.
alienbaby 56 minutes ago|||
I suppose there is the chance you get to prep a system setup at one time, so you can access it at a later time?
edf13 1 hour ago|||
Yes - immediately had me in a mild panic that I’d missed another AI discovered crit bug!

Had to quickly scan the rest of the homepage of HN to confirm.

tcdent 1 hour ago|||
Really, "password auth enabled" should be enough to be a non starter these days.
qwm 1 hour ago||
you'd be surprised
itemize123 2 hours ago|||
only 1) is a narrowing filter. the rest is basically the majority.
VorpalWay 2 hours ago||
Most linux distros use PAM, so 2 is also a narrowing filter. Debian, Ubuntu, Fedora, Arch, etc all use PAM.
jack1142 1 hour ago||
What if you narrow it down to musl-based distros (which none of the above are). Do most musl-based distros use PAM?
VorpalWay 1 hour ago||
Alpine is mostly used in containers, where you don't run sshd anyway. It apparently doesn't use PAM by default though.

OpenWRT also doesn't use PAM by default apparently, and there is a risk of people using password login, which you should never do past initial setup.

But outside specific niches, musl is rare, with good reason. Performance is a joke compared to glibc, especially the allocator. The only reason to choose musl in my opinion is for memory constrained devices (OpenWRT etc) or for building static binaries where performance doesn't matter but portability does. I consider the "small container" use case questionable: performance will suffer, and you could share large base layers instead which would amortize the cost over many different images.

kasabali 49 minutes ago||
AFAIK OpenWRT doesn't use openssh
j45 1 hour ago||
I think you're missing the reason why it's relevant now.

LLMs will find it.

They work endlessly at finding the 'and' scenarios.

This should have been closed up a long time ago.

mpyne 3 hours ago||
OK, I think I see what’s going on with this, in this configuration.

The goal from an attacker’s perspective is to get this to be true: `$ret = crypt($pw, $hash); $ret == $hash` when called by OpenSSH’s sshd. This is how sshd validates that the user logging in knew the account password.

The `$hash` is the “settings” to the crypt call and will normally look like “$6$--gibberish” for users with passwords set, but could look like “*” or “!” for users not intended to be able to login.

The bug is from the intersection of all of these things:

1. musl can return “*” for errors, rather than leave the return value NULL. Other libc have adopted something like this too, but seem to use a different result “*0”.

2. musl can also return “+” for errors, but only does so if $hash starts with “*”. This is intended to eliminate the possibility of an error causing `$ret == $hash` to be true, however it assumes that applications calling `crypt()` will set $pw and $hash to values corresponding to the `$ret == $hash` comparison to be made immediately afterwards.

3. OpenSSH can be convinced to pass the hash of a different user if the hash is “*” or “!” to avoid timing attacks that can enumerate the user list, so it will set $pw and $hash to correspond to different users. $hash will be a real “$6$--gibberish” style hash in this case, and OpenSSH expects `crypt()` to either return NULL or return a hash string. But now there’s two hashes OpenSSH uses. It does: `$ret = crypt($pw, $hash2); $ret == $hash1`;

4. This should normally not be a problem because OpenSSH will pass in valid $pw and $hash2 that should not lead to errors in hashing, so the $ret that musl generates should be a real hash that cannot match the “*” or “!” $hash1 for the login-disabled user. However as a DDoS mitigation, musl will return error if the password is too long when using SHA-2-based hashes (the default, it seems).

5. This error will result in a non-NULL result of “*” or “+”. Since OpenSSH passed in a real hash ($hash2), musl will return “*”, which compares equal to the actual user hash $hash1 for the disabled-login user.

The OpenSSL adjustment ensures a minimum hash length for the user before logging them in, even if crypt() succeeds somehow.

chuckadams 2 hours ago||
I think the conclusion that can be drawn from this is that the behavior of crypt() is howling-at-the-moon insane, and it should no longer be considered for any system built in this century, at least not without extensive wrappers that correct and normalize all these edge cases.
markasoftware 46 minutes ago||
Conclusion is type safety is important
zbentley 8 minutes ago||
Specifically, that the lack of tagged unions (or really, structured return types in general; you can pretend those are a tagged union if you believe hard enough) in C library interop conventions is a terrible foundation on which to build widely-used APIs.

And that just ends up indicting the C language itself. Can't send errors down a separate channel; folks might not inspect errno. Can't return NULL on error, folks might compare it blindly to another result. No tagged unions, so can't return one. Can't crash on error because there's no scoped error handling. Can't return a struct for checking .error/.result separately, because alignment and equality (and tearing and accidental clobbering and more) make that brittle as hell for widely-used APIs.

C is not a good language for library APIs. Like, really not. It has its uses/strengths, sure, but without even getting into the typical gripes people have about C, libraries--one of the things that it is used for the most--are one of the things for which it is the least well-suited.

"But it has a stable ABI!" So does a land mine. Until you touch it.

idlewords 3 hours ago|||
This is a vastly clearer explanation than the linked page. Thank you!
alexgoodhart 2 hours ago|||
Because Claude wrote the linked page. Claude is impressively bad at communicating these days. with gpt-5.6+ approaching Claude level like 30% of time and Gemini much clearer but also the most annoying 7 year old ever, it’s a pretty lame environment especially for people who r new to everything
pizzafeelsright 52 minutes ago|||
I wondered about this myself as the explanations and documents I have been maintaining are becoming increasingly 'slop', but a different kind of slop.

In the past I could get an explanation, where I was able to track the line of thought. Now when I read through the explanation it is like someone has taken two or three explanations and merged them into each other, like there is a combining of iterations without clarity.

example:

The tree is blue.

The tall tree has the color of blue.

the tree, while tall, is blue.

That finally gets squished into: the blue tall tree has height and is evident of blue.

Is this double speak or injected double effort of words?

endymi0n 2 hours ago|||
Until Opus 5.0 I'd have agreed with you, but Opus 5.5 is honestly one of the best I've ever sparred with, vastly better than the rest, including Astra imho. Especially so when prompting for clear top-down, first-principles communication.
ericbarrett 1 hour ago|||
Disagree, I review a lot of Opus 5.5 code and its comments and docs are still pretty inane and Claudish. Is it better than the 4.x and 5.0 era, sure. But it's still nouning the verbs, using colons everywhere, a-not-b, "a foos the b" etc. A lot of lingual knots that need a human hand to untie.
brabel 54 minutes ago||||
I was using opus5.5 for everything but since I tried google Argon4 that has been my favorite for writing. It’s clear and to the point.
yearolinuxdsktp 1 hour ago|||
Opus 5.5 is much better, but IME I always have to force it to review its writing (Opus 5.5) to get it to put the most important facts first (and not bury the lede). It loves to park the main point far away after intermediate clauses. It always writes “premise, so consequence.” It takes a prompt asking to review against rules in context for it to fix it.

The ‘concise’ output style setting is bad with Opus 5.5, as it will often spit out a cryptic (is this really English?!), allergic-to-commas concise response, which takes more time to parse.

xnorswap 1 hour ago||
Oh it loves to bury a lede.

    < mundane detail>
    < If you don't run this command your computer will explode>
    < exposition > 
    < mundane reminder to follow up something else >
Then 3 back and forths later, your computer explodes, and claude has the cheek to point out when questioned that it did warn you.
markasoftware 45 minutes ago|||
Yep, the linked page doesn't /feel/ AI written (maybe it's using a grok or muse model whose tone I don't quite recognize yet?) but it kinda just states a lot of facts that make sense on their own but don't cohere into a whole vulnerability like the comment above does.
byronsharman 1 hour ago|||
Thank you for explaining it with proper capitalization. I understood your explanation much better than the author's.
dboreham 1 hour ago||
> musl can return “*” for errors

wtaf ?

odo1242 1 minute ago|||
Returning a string on error honestly is defensible, the question is why the string was “*” instead of like “crypt ERROR: input password cannot be longer than 256 characters” lol
za_creature 56 minutes ago|||
There is no doubt in my mind that this was intentional.
zbentley 3 minutes ago||
Well, it was intentional, but not in the way I think you mean.

musl switched to returning `*` so folks stopped accidentally using NULL results, which was causing crashes and risked false-positives comparing NULL to itself.

That was a misfeature and should never have been implemented. But I bet it was an easy band-aid change to make!

BoppreH 4 hours ago||
In-band signaling[1] strikes again. * should not have been used as special value, neither for "no password" in /etc/passwd nor "error while hashing the password" in crypt. I'm afraid we'll find many more of these abuses, specially in string manipulations. The null was ok, an exception (or equivalent) better, and Result<T> (or equivalent) best.

[1] https://en.wikipedia.org/wiki/In-band_signaling#Computer_dat...

hedora 1 hour ago||
Exceptions are almost never the right choice. They create extremely complicated control flow extremely quickly, and then you have to test all the different control flows. Java software is an example of how this goes sideways fast. There's no way to create a class that prevents callers from reading the object before the constructor runs (in multi-threaded software, new first hands a reference to the caller, which can put it on the heap, then it calls the constructor), and objects are default initialized, so all fields (even static ones that are assigned in the constructor) can be null.

So, any field de-reference in Java can produce a null pointer exception. Try testing all paths in something like:

try { foo.sideEffect1().sideEffect2()...sideEffectN(); } catch { bar.sideEffect1().sideEffect2()...sideEffectM(); } finally { quuz.sideEffect1().sideEffect2()...sideEffectP(); }

There are M * N * P paths. The only way to write testable code with exceptions is to panic on uncaught exception, and to never catch unexpected exceptions. Even then, you're relying on nothing unexpectedly throwing the exception type you caught.

I'd argue Result<T> >> NULL >> sentries >> exceptions for this reason. Sentries are terrible, but at least they're not computationally hard to use correctly.

BoppreH 43 minutes ago||
I'd argue that's the fault of having default nullability, not the exceptions. You would have the same problem if every attribute access returned a Result<T> too.

And I'm not a fan of breaking control flow, but I still find it preferable to nulls. A null may travel far from the place where it was generated before blowing up, making troubleshooting harder, or it might come from a function you called for its side effect but forgot to check the return value. I gotta say I'm also a careful enthusiast of checked exceptions, it's great knowing which ways a function can fail.

But I think the consensus is growing that Result<T> (plus warnings for unused return values) is simply the superior choice.

__MatrixMan__ 3 hours ago|||
PSA: you can typically declare different types of string so that your type system objects to mixing data of different types even though deep down we know they're both strings.
BoppreH 2 hours ago|||
If you mean type aliases, that's a good idea within a single codebase, but the distinction between different types of strings doesn't survive serialization so it wouldn't have prevented this error. What they need is a way to signal errors out-of-band, like Result<T> or exceptions.
__MatrixMan__ 1 hour ago|||
In python, aliases are separate from typing.NewType, but you're right: it's only enforced within a codebase and then only if the dev cares to run the typechecker.

So yeah, I guess it either needs to happen at runtime, or we just do signaling and user data on separate file descriptors.

Normal_gaussian 48 minutes ago|||
This is presuming that the type system is nomitive rather than structural and there is not serialisation/deserialisation or type refinement occurring. A structural difference is much more preferable.
mkl 2 hours ago|||
Even using "no_password" and "error_while_hashing" would have prevented it. The problem was picking a generic, meaningless signal string.
mort96 2 hours ago||
That would've happened to fix it, because "error_while_hashing" and "no_password" happen to not be valid password hashes. The right way would've been to have different syntax for hashes and non-hashes; you could've made it so that a hash starts with a '#' and if it doesn't start with a '#', the rest of the string should be ignored.

Or, for backwards compatibility, start non-hash strings with a character which can't appear in a hash, such as '@'. Your examples would then become '@no_password' and '@error_while_hashing'.

SoftTalker 1 hour ago||
Well neither * nor ! (the two characters I've seen to indicate "no password") will appear in a valid hash. The problem was using a character that could also be returned as an error indicator, IIUC.
throw10920 3 hours ago|||
This is a side-effect of Unix-y "it's all plain text" design that leans extremely heavily upon in-band signalling (among other design decisions).

If /etc/shadow was a JSON (or XML, even, although that would make me feel gross), this bug would have been much easier to see, because a field that reads '"hash": ""' sticks out a lot* more than a custom microformat that you don't understand.

With data structures, there's never "plain text" - it's all structured. The question is whether you want to use existing formats and tools, or create a new one for every tool and literally make footguns like this.

dns_snek 3 hours ago|||
The issue isn't the config, but the fact that crypt() doesn't have any way to signal failure. In better designed languages you would have exceptions, result types, or something. Even in C you have "return integer indicating success or error" which would've prevented the confusion.

Now the public API is hard to change but you could still avoid the issue by returning a longer token like "==DANGEROUS_CRYPT_ERROR_length==" or "==DANGEROUS_CRYPT_ERROR_unsupported_alg==" which would never match the password field unless someone was really trying to induce the issue.

knome 2 hours ago|||
>but the fact that crypt() doesn't have any way to signal failure

https://pubs.opengroup.org/onlinepubs/009604599/functions/cr...

>Upon successful completion, crypt() shall return a pointer to the encoded string. The first two characters of the returned value shall be those of the salt argument. Otherwise, it shall return a null pointer and set errno to indicate the error.

returning a sentinel value of NULL and setting errno is pretty standard C, and shouldn't cause an error.

OP's sshhole link specifically notes that it _should_ return NULL, but some MUSL devs decided they wanted it to return trash instead, and the trash they returned instead of NULL caused this.

All because some devs were too lazy to check the result and another dev decided to be clever and "fix" NULL errors from crypt by sending back 'unmatchable' trash banking on devs not checking NULL.

edit: apparently crypt implementations have been trash ignoring the NULL return for a long time. this isn't only on MUSL, but it's still because people ignored and tried to work around the existing out-of-band error mechanism.

dns_snek 37 minutes ago|||
> returning a sentinel value of NULL and setting errno is pretty standard C, and shouldn't cause an error.

> OP's sshhole link specifically notes that it _should_ return NULL

I meant there's no mechanism besides returning NULL that would make it hard to ignore the issue (exceptions need to be caught, results need to be unwrapped and their failure handled, an integer return value makes it clearer that there's something there that you need to check). I also missed errno codes when glancing at the manpage earlier which should illustrate the problem.

And yes, whoever first had the brilliant idea to start returning garbage never should've done that, I'm not disputing that.

mafuy 1 hour ago||||
If this is true, then the entire thread branch here on HN is entirely wrong. What is going on??
hedora 1 hour ago||
People don't RTFM, either on HN or when implementing "standards compliant" stuff like MUSL.

No error handling scheme can fix that problem. For instance, if a Rust API says I should return a Result<String, ENOSYS>, I can always write code like `if(ENOSYS) { return Ok("*") } instead of returning Err(ENOSYS).

Then my code would be compatible with MUSL.

dns_snek 32 minutes ago||
> I can always write code like `if(ENOSYS) { return Ok("*") } instead of returning Err(ENOSYS)

You can do that but it's going to feel bad to write that and it's going to stand out like a sore thumb to anyone who reads it. But "errno" is just ambient state that's completely invisible unless you happen to have the manpage fresh in your memory.

FLeXMurphy 41 minutes ago|||
Except it's not trash. It's a backdoor funded by our glorious leader Kim Jong Un.
BoppreH 3 hours ago||||
No, that's just more in-band signaling!

Some badly designed password hashing algorithms (cough bcrypt cough) have maximum password lengths, ignoring everything after X characters. So it could happen that I set my password to a 100-character string, it gets saved as "==DANGEROUS_CRYPT_ERROR_length==", and then anybody can log in to my account by entering an unrelated long string that returns the same "error".

If a function promises a string result, it should only provide a string if the function succeeded. The null was much better than magic strings.

dns_snek 2 hours ago||
I know, it's bad. I agree that it should've stayed NULL. But if you really really really think you have an acceptable reason to do something like this (and you probably don't), the least you can do is use a non-trivial token.
BoppreH 2 hours ago||
If I was really desperate and could only return a valid string, I would probably use a message and add a random component to avoid ever matching two errors. Like SQL, where null != null.
panzi 1 hour ago|||
Personally I think there was nothing wrong with returning NULL. "But if people don't check for it it crashes their programs!" Good! Let it crash loudly! Then they notice and fix the issue.

Also I think it could have returned ":". Given the file format of /etc/shadow and /etc/passwd I don't think it's possible to have a : as a value for anything.

hnfong 1 hour ago||
> Also I think it could have returned ":". Given the file format of /etc/shadow and /etc/passwd I don't think it's possible to have a : as a value for anything.

I wonder if somebody else would have thought the exact same thing in a different (non-/etc/*) context, and then you get the same effect in a more obscure manner.

__MatrixMan__ 2 hours ago||||
A few years back I un-bashed my life as much as possible in favor of nushell (though it's not the only shell to encourage structure). I feel like it has really helped me notice a lot of this-needs-more-structure type problems outside of my shell that I otherwise wouldn't have.

The elegance of the unix-y approach is actually that it's all bits. You can just join things together and shove the bits through, that's a wild simplification compared with having vendor-specific kinds of plugs. But we could have that kind of compatibility while also benefiting from structure.

So I encourage everybody to give a shell with structured output a try, let it rewire your brain, and then ask yourself: how is the OS holding this back from being everything it could be? Because if we build that OS, it won't encourage these problems.

When I imagine it, it would probe either side of a pipe to see if they support negotiation. If not, send the bits through in the same old unixy way. If so, use the newest schema for structured data that is supported by both sides.

jonhohle 2 hours ago||
At Amazon we had RecordStream which implemented most of the standard Unix tools on structured streams. It was pretty easy to write filters, aggregators, and formatted output in something not much more complicated than sed/awk/cut/join/sort pipelines. There was a convenient structured log format for services that was often used as input.

https://github.com/benbernard/RecordStream

hedora 1 hour ago||
That looks really cool. I've been heading the opposite direction: posix shell for everything.

I have a bunch of management scripts, etc, that need to run on everything from a potato to a T-1000, and that's the only language that's always available. (OpenBSD sh, FreeBSD sh, busybox, bash, dash, zsh all can cope with it).

I'd love something that's at least JSON-typed (or equivalent), but that can be bootstrapped up on least-common-denominator posix.

Has anyone built something similar? (Yes, I'm being picky -- bun / ts is already a pretty low bar for cross platform support, but these scripts do stuff like install node, so I'm not totally crazy for asking.)

UqWBcuFx6NV4r 3 hours ago||||
At this point I’m resigning myself to think that we’ve just gotta wait for some people to die. It feels very dramatic and glib (no pun intended lol) and whatever to say, but at this point it feels like there’s a certain hostility in some parts to changing anything from the platonic ideals of however computing happened to work in 2004. “Oh we don’t need to mess with it!”
xorcist 6 minutes ago|||
How did you arrive at that conclusion? If anything, this vulnerability happened because someone decided to change the behaviour of crypt() without fully understanding the consequences. This specific vulnerability did not exist in 2004.
gspr 3 hours ago|||
I think this is a bit unfair. The way I see it, a lot of people are saying that changing breaks lots of old stuff, and risks complete fragmentation. Those are real costs to change. It might still be worth it, but let's not pretend that everyone is resisting change just because it's change.
abirch 2 hours ago||
The costs to change are dropping significantly with AI.
gspr 2 hours ago||
Citation needed.
Philpax 1 hour ago||
https://commandline.microsoft.com/agentic-software-engineeri...
gspr 1 hour ago||
All I see is sausage makers advocating for a sausage-based diet.
yearolinuxdsktp 1 hour ago||||
Returning “*” instead of NULL to signal failure is the real footgun. Don’t blame the format.

For historical reasons, we could’ve ended up JSON as “hash”: “*” just the same. The /etc/shadow format lets you skip values completely, like “::”, so your argument doesn’t hold.

Terminating an app because of a NULL dereference is the other footgun. Sadly, we still have “panic” in modern languages like Go and Rust.

hedora 50 minutes ago||
"panic" is a good thing. There's always some error condition that your code cannot handle. At some point, the least bad thing to do is crash.

For instance, what if your error handling code throws?

One spectacular example of panic saving the day was the Apollo 11 1202 error. It was basically a watchdog timer that fired if the landing computer was starved of CPU time. A misconfiguration caused the first moon landing to hit CPU overload. When that happened, the computer displayed 1202 and rebooted, then ran things in priority order. Apollo 11 landed in the middle of a tight 'panic, reboot, adjust thrusters' loop. Thanks to core memory, most of the heap survived the reboot, so the reboot step was fast enough for it to mostly land without human interaction. The pilot had to make ~1 adjustment to get it to actually hit the landing site, but the panic-ridden computer handled the rest of the descent.

https://dpma.de/english/our_office/publications/ingeniouswom...

BoppreH 3 hours ago||||
Yes, absolutely. Take for example this cURL command:

  NO_PROXY=1 curl -qs --data @./my\ message.txt -- \
  "https://user:password@example.com/user/${userid}/my%20message.txt"
Random parts of it will be interpreted by the shell, others by cURL, or the filesystem, or the remote server. They each have their own encoding rules, nested. I count ten distinct separators and nine ways of escaping/tagging. It's one command. No, just no.

We have seriously overused strings and we're now paying the price.

dolmen 2 hours ago||
Also: free text is the input format of LLMs...
raverbashing 2 hours ago||||
> "it's all plain text" design that leans extremely heavily upon in-band signalling

Honestly I've been shaking my fist at these since 0 becomes True/Success in Bash and the opposite everywhere else

xorcist 5 minutes ago|||
Error code 0 means success in a lot more places than just bash.
cwillu 1 hour ago|||
That's not specifically bash though, that's process exit codes
killerstorm 3 hours ago|||
I'd say another UNIX-y thing - "C is good enough" - is part of the problem. C makes programming painful which results in all sorts of hacks.

E.g. in Pascal you can can define a record and then defined file of record to have typed I/O. I.e. it comes with built-in serialization format supported by compiler. Not a good one - but perhaps a step in a good direction.

OTOH C people starving for serialization often believe that `struct` byte layout made by a specific version of a compiler is a serialization format - a view not endorsed by the standard.

amluto 1 hour ago||
The whole story is a bit worse than this. Musl already worked around this type of failure. See the second-to-last line here:

https://git.musl-libc.org/cgit/musl/tree/src/crypt/crypt_des...

(hey, this workaround is wrong if the salt is "x"!)

but they forgot to work around it here:

https://git.musl-libc.org/cgit/musl/tree/src/crypt/crypt_sha...

The vulnerable path in musl is only a little bit vulnerable, because "*" is not actually a sha256 encrypted configuration and should not reach this code: musl's crypt(..., "*") will not call that code! Turns out that sshd detects that the password hash is too short and does not forward the salt!

https://github.com/openssh/openssh-portable/blob/master/auth...

Look, no else statement! If that if doesn't get taken, then salt remains NULL. And the sshd code is really quite messy and doesn't keep track of the fact that this code path is inappropriate for validating an existing password hash.

A workaround in the sha256 path in musl wouldn't even have prevented the bug!

I'm going to give some credit to musl for unwisely using "*" and some credit to OpenSSH for having nonsensical control and data flow.

NewJazz 1 hour ago|
The formatting in your comment got messed btw
amluto 1 hour ago||
Thanks, fixed
nxobject 4 hours ago||
Above all, I’m a little sad that the musl developers decided to make a lot of “well it’s convenient” changes to core crypto functions — at least without the level of auditing that would have caught this! It’s tarnished their reputation in my eyes a little.
trebligdivad 4 hours ago||
In many ways each individual decision makes some sense; wanting to stop loads of programs being affected by a (remotely triggerable?) crash wanting to avoid time based attacks wanting to be able to set accounts to not have a password

Each one relies on a perfectly valid argument - that * can't match because we all know it'll be rejected by the other thing.

yangm97 3 hours ago|||
I would be more concerned about the lack of explicit return types, one of the arguments for strongly typed languages is that you should not have ambiguous data and yet…

Granted I mostly work with high level programming languages so I may be missing something.

Retr0id 4 hours ago||||
> Each one relies on a perfectly valid argument - that * can't match because we all know it'll be rejected by the other thing.

But that's not a perfectly valid argument, and it never was.

zarzavat 4 hours ago|||
The real problem here is the C language. Our industry should have dumped it a long time ago and we’re now paying the price of prevarication.
oneeyedpigeon 3 hours ago|||
Which of the several flaws in this bug chain would have been materially different with a language other than C?
nh2 3 hours ago|||
Any use of sum types would have prevented this immediately.

The whole thing exists because APIs that return a NULL value that "nobody checks", plus a string that you shouldn't use if you actually did check the null value.

In languages with sum types, this cannot happen.

Either you get your hash, or no result at all. You couldn't get eg NULL and *.

jppittma 3 hours ago|||
C has unions. C has sum types. People have been doing tagged unions in C for longer than rust has been a language
creata 1 hour ago|||
It is extremely unidiomatic to use tagged unions in C for error handling. It's definitely possible, but awkward, and the compiler doesn't check that you checked the tag.
K0nserv 2 hours ago||||
I don't think I've ever seen a C function that uses unions for this purpose though?

I can think of two common patterns that I've seen frequently:

* Return NULL for errors (sometimes with a global errorno() function).

* Return an integer error with any return values as out parameters.

So if it is possible to improve, why doesn't anyone do it?

aliceryhl 2 hours ago|||
Yet the authors of crypt did not use them. The C language is not designed in a way to encourage people to write code that correctly uses tagged unions (or nullable pointers for that matter).
jppittma 2 hours ago||
Where are you going with those goal posts?
Philpax 1 hour ago|||
Nowhere. It's the same argument as upthread. A language that forces downstream consumers of an API to correctly deal with all possible outcomes would have prevented this.
rini17 1 hour ago||||
Goalposts are fine. If crypt did return union type, C still allows cowboy programmers to straight extract the char* without any check whether it's valid. So we'd end up in exactly the same situation.
cannonpalms 1 hour ago|||
I don't think the goal posts were moved. The comment you replied to referred to the "use"--not the existence--of sum types.
dolmen 2 hours ago||||
Nope.

Because in fact all the changes were about changing the function signatures. The function signature is enforced by the linker. The semantic of strings that go through isn't.

Returning 2 values is possible in C (via pointers input parameters). The main issue here is that libc is a strong API contract based on C function signatures, that nobody can change, only augment with new symbols. Such strong API compatibility constraints happen in any programming language, and workaround to fit new behaviours in old APIs happens everywhere.

K0nserv 1 hour ago||
I don't think nh2 is saying that this is how the "issue" should've been solved here, but that it's a fatal flaw of C itself (yes I know, unions, but no one uses them).

But also, its not like there were many alternative languages when the POSIX standards were designed so it's fair that it works like this.

muvlon 3 hours ago||||
C's lack of all of

- sum/union types

- multiple return values/tuple types

- exceptions

means that most ways of returning an error from a function are not available in C. Because of this, many C APIs, including `crypt` in this case, resort to in-band error signalling by using a sentinel value (like NULL, a negative integer, "*" etc.).

Most other languages lack one or two of the above, but very few lack all three, and so most have developed a different culture around error reporting (Result in Rust, multiple return values in Go, exceptions in Java, Python etc.).

tialaramex 2 hours ago|||
One of the small but important significances of Rust is that it very often gets to have the same machine code as a correct "in-band error signalling" strategy from C but with source code that the maintainers can actually understand.

And really often when your "in-band error signalling" in Rust is more expensive you realise (perhaps to your horror) that your C wouldn't be correct, so you just dodged a bullet. For example Option<OwnedFd> is, like OwnedFd, a 32-bit value, Unix has always reserved -1 to mean invalid, so Rust can treat that as a niche. But the Windows equivalent Option<OwnedHandle> can't do this because, well, cue Raymond Chen: https://devblogs.microsoft.com/oldnewthing/20040302-00/?p=40...

creata 1 hour ago||
> One of the small but important significances of Rust is that it very often gets to have the same machine code as a correct "in-band error signalling" strategy from C but with source code that the maintainers can actually understand.

But aside from NonZero- and NonNull-based layout optimizations, I don't think Rust is doing anything special to obtain that machine code - it's just relying on LLVM to optimize things.

sscaryterry 2 hours ago||||
The first 2 it has, and the 3rd, you really don't want.
muvlon 1 hour ago||
The first 2 C has in the same way that I don't need Dropbox if I just have an FTP account and curlftpfs (https://news.ycombinator.com/item?id=9224).
sscaryterry 54 minutes ago|||
Your analogy is way off...
jppittma 3 hours ago|||
C has sum types/unions? What are you talking about? You don't need multiple return. You can use a struct and it's the same thing.
muvlon 1 hour ago|||
C unions are sum types if you just "draw the rest of the fucking owl". C has sum types the way x86 assembly has function calls. You see, there are "call" and "ret" instructions. Just figure out calling convention and the stack yourself, it's easy. And yes I can stick the tag next to the union myself in C and write a bunch of functions that hopefully deal with it correctly. I can even write some macros that look like pattern matching and maybe convince myself that it's a language feature, but it just isn't.

And it's very evident why the difference matters. People gravitate to what's readily available and ergonomic. That's why every C API under the sun (most definitely including libc itself) does janky stuff like returning -1 or "*" on error instead of building their own bespoke Result-style abstraction. A programming language is not just the things that are in some way possible within it, but more importantly the things that are easy in it.

uecker 1 hour ago||
While I agree that this should come out of the box, the "rest of the fucking owl" is a tiny macro or function, so not exactly something difficult to do complex as "drawing an owl" would imply.
muvlon 1 hour ago|||
There are different amounts of effort you can pour in to this depending on your requirements, and we can argue about those, but to me the evidence is clear: It's too much of a hassle for library authors to actually go and do it. Everyone just uses sentinel values instead.
adgjlsfhk1 1 hour ago|||
not in a way that forces the user to actually handle them properly. C is a weakly typed language and as such is pretty badly suited to systems programming.
uecker 1 hour ago||
C is not a weakly typed language, this is just nonsense. There is also [[nodiscard]] which you can use to make sure a return value is handled.
muvlon 44 minutes ago||
What is a weakly typed language to you? To me, implicit type conversions are a big part of the definition, and C is flush with those (void* converts to any pointer, all kinds of number conversions, arrays decay to pointers, functions convert to pointers, lvalue conversion strips const/volatile etc.).

However, I also think "weakly typed" is not a well-defined concept, everyone seems to apply it differently. In particular, it seems to mostly mean "playing it loose with types in a way that I don't like".

uecker 34 minutes ago||
Yes, it is not well-defined. But most conversions, function calls, and accesses are strongly typed in C, so I would not call it weakly typed.

Implicit conversion could be seen as weak typing, but it is mostly limited to specific arithmetic operations in C. I would see implicit conversions as problematic if they allowed operations that make no sense. Decay of arrays and function to pointer conversion also do not seem problematic to me for this reason.

Void pointers are also weakly typed, but they are specifically designed for this. So if you do not want this behavior, one would simply not use them. It is a bit like using "unsafe" in Rust.

There are certainly stronger typed languages, but on the full range C is more on the strongly than weakly typed side in my opinion. A language that allows arbitrary implicit conversions between pointers, would be weakly typed, or which allows calling functions with wrongly typed arguments (which was true in the past for C a long time ago)

K0nserv 3 hours ago||||
For the other part of the issue (the truncation in SHA512), in Rust you could've had:

    struct Input<'a>(&'a [u8]);
    

    impl<'a> Input<'a> {
      fn new(input: &'a [u8]) -> Result<Self, Error> {
        if input.len() >= 256 {
           return Err(Errro...);
        }
        
        Ok(Self(input))
      }
    }
Now the truncation is a terrible idea in the first place, but at least with this it would have been entirely obvious to the caller that it was there.
uecker 1 hour ago||
In C you could do just the same: https://uecker.codeberg.page/2026-10-24.html Whether you should is a different question.

In any case, you can not change the API of an existing function. But returning NULL for error has required by POSIX would have been just fine.

dmantis 3 hours ago|||
Wouldn't Rust's Result or Go's error checking prevent that?

While not mandatory, a person would probably use it instead of the * hack.

Simply because the calling side wouldn't have an option to forget the check like with the NULL case

ninkendo 3 hours ago||
Rust and go still make it pretty easy to not check results if you don’t care about the success value (lots of code just does `let _ = foo()`), but if you do want the value (like with a hashing function), Rust makes it really hard to not check the error too. There’s no syntax to say “assume this worked and give me the success value, including whatever garbage is there if it’s not set”:

- result.unwrap() will panic if it failed

- result.ok() throws away the error but gives you a None value instead (still can’t use it as if it’s a success)

- matching on the Ok variant and doing nothing on the Err variant, you only get to use the success value inside the Ok arm

Etc etc.

The problem is when the compiler says “NULL is a valid instance of T”, which go lets you do, and rust doesn’t (without using the unsafe keyword and intentionally casting a null pointer, which you don’t do by accident.) Go lets you write code like `var hash string; hash, _ = hashfn()` and it compiles and now you have null instead of a valid hash.

tialaramex 3 hours ago|||
> NULL is a valid instance of T

This by the way is Tony Hoare's famous "Billion Dollar Mistake"

uecker 1 hour ago|||
A NULL pointer for error would be a safe interface where you get a trap when there is no value.
f1shy 1 hour ago||||
I understand that other languages could help avoiding the semi-predicate problem. That being said, in C there are better ways around semi-predicates, as inelegant as they may be, is possble, is not an inherent problem of the language, but may a bad habit/idiom of it.
randomNumber7 2 hours ago||||
Do you work in industry or academia?
01HNNWZ0MV43FF 3 hours ago|||
There is no industry, it's just people
rustyhancock 4 hours ago|||
I think everyone is over fixated on AI finding complex bugs beyond a typical person's understanding either to locate or exploit.

I think for any one of those bugs there might simply be half a dozen footguns or bugs that once it's spelled out you wonder how anyone let it slide.

I think that's the case where LLMs might indentify most.

It's challenging though as the current AI audits produce so many false positives it's hard to know when it's being pedantic or when it's genuinely caught a human being a bone head

jamal-kumar 3 hours ago|||
Finding more and more that these complex bugs people gloss over like you are now are being used in exploit chains and the end result is still my clients getting hacked
svnt 2 hours ago|||
This one might be hard to find using the author’s approach of throwing everything at various builds of ssh.

It is not really that hard to find in the wild if you had a device built this way, when overflowing buffers on embedded devices is a standard attack vector.

It might be hard to root cause, but it would not be that hard to find.

throwawayffffas 23 minutes ago|||
I am sorry, a little? I am deeply disappointed that libc decided to do the same just with another sequence.

And why because some dude was complaining sshd or whatever was crashing because it didn't handle nulls? Just handle the null my guy you already know why your bullshit is crashing.

WesolyKubeczek 3 hours ago|||
You must be new here. While I applauded the idea of musl when it first came out (coincidentally, glibc wasn't having the best time in its development then), the developer's arguments why <some behavior drastically different from every libc under the sun> was akchyually correct, which was why you had to fix a program that hadn't been broken for 30 years and now suddenly was, made me put him under the "clowns" category and never look at musl seriously again.

When later it turned out that musl's procrustian stack size made Python not really runnable under it, it cemented my resolve to not go near musl.

And now we have this.

acdha 3 hours ago|||
I’m also reminded of the clowns pushing “it’s smaller!” container images based on social media vibes rather than analysis where, yes, not using Debian+glibc saved tens of megabytes on the base OS but did so at the expense of measurable performance reductions & compatibility issues like the one you mentioned, and literally every single one of those dudes I worked with was shipping containers pushing a gigabyte so the size reduction had no measurable impact on deployments.
NewJazz 1 hour ago|||
Uh, isn't this the case of the opposite? The musl devs trying to accomodate broken programs?
rpcope1 1 hour ago|||
Honestly the ideas around musl are interesting, especially being able to statically link, but given the sort of nonsense around how they handled pthreads, and generally other trade offs, it's hard to imagine wanting to use it over glibc (which everyone shits on) if you don't absolutely need static linking.
semiquaver 4 hours ago||
A little?!? I’m sorry, but musl is a joke, this is yet another proof of that fact, and I have no idea why anyone uses it in production, they have only themselves to blame after the many warning signs.
wowczarek 29 minutes ago||
In my last org I was in charge of internal testing and qualifying software releases for network equipment, and designing test harnesses and tests for these. Significant install base. All the major vendors. Many years. Basically doing the vendor QA for them, because we couldn't afford things to fail. As you do - I wish.

This was not security testing as such - mostly latency, performance and hardcore network "can we break it" and "does it comply" stuff, but also a lot of cable and module / line card pulls, pluggable and fiber breaks, random reboots and pre-empting a lot of possible operator inventiveness. At one point we considered SEU bit flip testing on rented cyclotron time.

And always we battled against the top brass urging us to speed up the process and don't bother with "really basic" testing, because we observed software quality going down over the years as the push for release and time to market stared to dominate.

So we always started with sanity testing - and that included oversized password and CLI input strings. Always. And we caught things more than once, but never like this.

We were right after all.

wolrah 3 hours ago||
Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option because it means people actually notice that there's a problem. Silent compatibility hacks are the worst, they guarantee problems never get fixed and the stack of hacks never gets smaller.

Microsoft has historically been one of the leaders on the wrong side of this one, but they have also demonstrated how effective it is to call out badly behaved software and make it annoy the users with UAC. When Vista came out, so many applications needed elevated privileges to run because they were badly designed to write user data in system locations, but now their application was more annoying to use and came with a scary dialog about it making changes to your computer that other better-designed software didn't get.

More often than not if a program didn't actually intend to change system settings it was just a matter of badly chosen default paths which are pretty trivial to fix, but so many vendors just would not do it despite years of complaints from NT admins wanting to not have to spend hours with Process Explorer figuring out specific permissions to grant limited users enough access. Once their failures were put front and center, calling their application out as the one getting things wrong every time the user ran it, they suddenly had motivation to fix it.

simias 3 hours ago||
For security it's better, but in many cases if you're not writing security-oriented software it can be worse from a user perspective. I remember the first job I had over 20 years ago was working on VoD streaming and one of the "tricks" to improve stability was mmap'ing the 0 page so that NULL dereferences wouldn't immediately segfault. Ugly as hell but when you were working with all sorts of more or less well coded media codecs and security wasn't a concern at all (worst case scenario the user would stream movies without paying) it made some amount of sense.

I suspect that Microsoft has the same history: the entire Windows platform was built with single-user PCs in mind, mostly only dealing with trusted input. The users were also expected to be tech-illiterate for the most part and unable to troubleshoot deep technical issues.

More recently I'm still working on video transmission, but on the production side this time. I also originally adopted the mentality that I should crash early if some unexpected situation was encountered, but I ended up walking that back a lot, because having a critical piece of live media production reboot mid FIFA worldcup final is generally Not Great. Degraded functionality is usually preferable. I still crash early in dev builds, but the production code just logs an error and continues if at all possible.

That said, I definitely agree with you for something like password handling. In this case, I do think it's warranted to crash the program without survivors if something is amiss.

pmontra 2 hours ago|||
> if you're not writing security-oriented software

It's hard to define what's security oriented and what is not. For example, is this HN web app security oriented? Not in the way of sshd but it must keep our accounts data separated and it must not let people exfiltrate data from the db, where for example it is storing the emails of some of us.

funcDropShadow 1 hour ago||
It depends on the time. Twenty or thirty years most software was not security-oriented. Nowadays, everything is online and therefore everything needs to be vigilant about security.
pixl97 2 hours ago||||
> because having a critical piece of live media production reboot mid

It's not great, but these days a trickster with AI will find some way to start broadcasting porn to a billion people because of the flaw, so may end up being the better option.

whoanoway 2 hours ago||
[dead]
stackghost 2 hours ago|||
> because having a critical piece of live media production reboot mid FIFA worldcup final is generally Not Great.

Story time?

pizlonator 2 hours ago|||
> Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option because it means people actually notice that there's a problem.

100%

zahlman 1 hour ago|||
Agreed. I feel like this attitude has been holding Python back, too. It's one thing to criticize "move fast and break things", but it's also not good to ignore already existing breakage just because the pieces still fit together neatly, and it's not good to refuse to move at all.
drdexebtjl 1 hour ago|||
Now everything installs to a user local folder, runs as the user, there are no UAC prompts, and malware can just freely tamper with other applications, act as the user, and harvest all data the user has access to.

Hey, but at least it can’t touch the System32 folder.

Great job guys.

hnfong 2 hours ago|||
> Microsoft has historically been one of the leaders on the wrong side of this one

Objective-C ahem nil messaging...

sscaryterry 2 hours ago||
Objective-C is an Apple thingie :)
hnfong 1 hour ago|||
Yes and thus I dispute that Microsoft leads on this front :P
usefulcat 2 hours ago|||
By way of NeXTSTEP
SoftTalker 1 hour ago||
NeXT certainly used it extensively but they didn't invent it. Originally developed by Brad Cox and Tom Love in the early 1980s, per !w.
quotemstr 2 hours ago|||
> Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option

Sadly, we will never learn this because failing fast runs counter to intuition, and a class of person will never override it.

zicixisi 3 hours ago||
[flagged]
sidneythekidney 3 hours ago||
Fun!

I was able to reproduce this on Alpine 3.23.7 with OpenSSH_10.2p1.

Luckily PermitRootLogin [0] hasn't allowed logins with passwords for root since 2015 [1]. I'm guessing that's at least part of why this went undiscovered for so long.

0: https://man.openbsd.org/sshd_config#PermitRootLogin

1: https://cvsweb.openbsd.org/diff/src/usr.bin/ssh/sshd_config?...

scandinavian 4 hours ago||
Did anyone get it to work? I tried on the official alpine docker image, enabled password login for root. The shadow entry has ! as password by default, this didn't work with a > 256 character password (i also did passwd -l which also does !). I manually changed it to * and it still didn't work.

Am I doing anything wrong here?

OpenSSH_10.3p1, OpenSSL 3.5.9 29 Sep 2026

PRETTY_NAME="Alpine Linux v3.24"

markuman123 3 hours ago||
It's because alpine has updated all openssh-server packages already. Even from EOL alpine versions.
bonyt 3 hours ago|||
Ahh! Wasn't working on 3.16, which does indeed have an updated package.

But, got it to work on 3.15 - which requires changing the ! to a * in the shadow file in addition to other steps (making another user with a hash, setting PermitRootLogin to yes, etc.).

https://pkgs.alpinelinux.org/package/v3.16/main/x86_64/opens... Build time 2026-11-05 06:53:46

markuman123 3 hours ago||
https://gist.github.com/markuman/acd2c2e7e08c849cfafe9e47bec...

on 3.21 with the previous version ...

justinsaccount 3 hours ago|||
oooh, that'll do it.

Didn't notice that the openssh-server-10.3_p1-r2 was released today.

atmosx 4 hours ago|||
Nope, wasn’t able to reproduced it in my firecracker VM, which isn’t publicly accessible anyway.
f311a 4 hours ago|||
As far as I understand, it wont work on default setups. You need musl-c, no pam and openssh.
zamadatix 3 hours ago||
In Alpine, these would all be the case for an out of the box install with openssh added unless you specifically install openssh-server-pam and configure sshd_config to use PAM (Alpine is already musl-c based and doesn't default to PAM).
Aachen 3 hours ago||
Doesn't it also need a user to be added with a password entry?

I only ever see Alpine Linux used for containers which don't need accounts with interactive user logins, which is not to say it doesn't exist, just that it doesn't seem like Alpine comes with all preconditions met out of the box

zamadatix 2 hours ago||
Isn't it the opposite, accounts which have no password and hence had * instead of a hash?

> accounts that should never take a password at all (root, your key-only deploy user, ldap users, the default user on alpine's aws and azure images) take any password longer than 256 characters.

But yeah, Alpine for persistant installs isn't unheard of - just not as popular as the container use case. They offer official VM, Xen, Pi, and bare metal images users deploy as well. I actually had a fleet of VMs to update!

Edit: The Alpine notice made it clearer to me that at least one other account has to have a password, it's just the target account that doesn't. So if you truly have no users with passwords in your container deployment then those passwordless users aren't vulnerable to this bug, but any of the image types which have a user will likely have root exposed.

> The attacker needs the name of an account whose password field is * (an account with no password set, which is normal for key-only accounts), and needs the machine to have at least one other account with a $5$ or $6$ password hash.

https://alpinelinux.org/posts/2026-11-05-openssh-password-au...

q3k 4 hours ago|||
EDIT: works after downgrading OpenSSH, turns out even EOL Alpine version got updated packages!

Also didn't have much luck on Alpine 3.20.10 with OpenSSH_9.7p1 which I've freshly installed from an official ISO.

Last two entries in /etc/shadow:

    test:*:20762:0:99999:7:::
    q3k:$6$snip.GgDQNTrqqX.A0:20762:0:99999:7:::
SSHing into test@ with one 'wrong' password then one 'long' password doesn't work. I have UsePAM no and PasswordAuthentication yes in my sshd_config.
emayljames 4 hours ago|||
Did you try one incorrect standard password fail first?
justinsaccount 3 hours ago|||
I can't reproduce it either. I used alpine:3.24.1 and openssh is openssh-server-10.3_p1-r2

I tried adding PermitRootLogin yes and PasswordAuthentication yes. Tried adding another user with a valid password.

Verified shadow entry has a *. Can't login as root or a test user. Tried another incorrect password first as well.

edit: can reproduce after `git revert 384e25e6bd1f2e7b30095900e48b570d46953510` in aports and rebuilding openssh without the fix. It also accepts the password on the 2nd+ attempt, NOT the first.

hagbard_c 3 hours ago|||
No, didn't work for me open OpenWRT x64 running in a container but that is probably 'cause I'm using dropbear instead of openssh-server. That's what OpenWRT uses by default so the impact there will be minimal assuming that most installations don't change to openssh-server - it isn't installed by default.
ryanw02 3 hours ago|||
[dead]
so_it_be 3 hours ago||
[dead]
evgpbfhnr 3 hours ago|
So to recap, with a non-upgraded sshd you need:

- an account with /etc/shadow hash set to '*'

- (note root won't work by default because of PermitRootLogin prohibit-password default)

- attempt twice, first attempt fails second long password is accepted (that took me a while to understand)

In practice passwd -l seems to lock with a ! prefix, and new account without passwords are created with a single ! which is also not affected, I'm not sure how big the impact actually will be in practice and don't understand the recommendation to upgrade urgently right now... Just don't use `*`?

EDIT: ah, some default container images apparently have a default alpine user with that hash, so I guess that's bad for them. This makes a bit more sense now.

babagan0ush 3 hours ago||
> Just don't use `*`

Well but, alas, a non-zero number of user probably do use it, though and "upgrade X" is a clearer and better instruction to communicate than "change this setting where maybe you don't even know what it means and it might break your workflow, but saves you an upgrade".

evgpbfhnr 2 hours ago||
In some contexts (e.g. embedded) you'd be surprised at how reluctant some people are to upgrade when they can run through a dozen of hoops for workarounds such as changing hash instead...
cowlevel 1 hour ago||
Why do you need to attempt twice?
More comments...