Repository navigation
Reloading page bypasses TLS client authentication #35317
Description
Activity
- addedtlsIssues and PRs related to the tls subsystem.Issues and PRs related to the tls subsystem.docIssues and PRs related to Node.js documentation.Issues and PRs related to Node.js documentation.
on Sep 24, 2020 I'm guessing it might be due to some TLS connection reuse logic.
Close, it's not the connection but the TLS session that's reused. :-)
Reuse cuts the handshake short (and cuts out the client certificate exchange) because it reuses the previously established session parameters. That's per spec and normally what you want. Chromium probably creates a new session when you reload.
You can force a new session by checking
socket.isSessionReused()and then callingsocket.renegotiate()but caveat emptor, only up to TLS v1.2 - TLS v1.3 doesn't support renegotiation.(req.socket as tls.TLSSocket).authorizedflag should always be false if the client certificate doesn't match the server CA. In this case it becomes true after a page reload from Firefox for unknown reasons.That's an issue on our side (and I can see why it's surprising behavior) but I hesitate to call it an outright bug.
socket.authorizedisfalsewhen a verification error happened during the handshake (e.g. invalid or untrusted certificate) buttrueotherwise.A new connection started from a resumed session doesn't do that verification and hence assumes
socket.authorized = true. The nature of TLS sessions is such that I'm not sure this can be fixed even if we wanted to.I do feel the documentation for
socket.authorizedshould mention that caveat. Currently it only says this:Returns
trueif the peer certificate was signed by one of the CAs specified when creating thetls.TLSSocketinstance, otherwisefalse.I've added the
doclabel. Pull request welcome!Reacted by kq and Allen PittardThanks for the answer!
However, I believe that if this is not a code bug it is at least a severe security flaw and should be addressed as such. I found many many examples on the web for client authentication which relied on the concept thatsocket.authorized == false, and god knows how many websites are relying on that. That might be due to a documentation gap, yet the effect on security is the same.Furthermore, thanks for explaining how to verify the certificate through each request. However I think that approach has two flaws:
1: Renegotiation is forced on each request, and I believe that's expensive for the server.
2: Such approach won't work on TLS 1.3 (which alternatives are there?)I believe that ensuring that keeping
socket.authorized == falseby default could solve all of these problems in a simple manner.Reacted by kqI won't argue you're wrong but keep in mind this only happens because your script turns off authentication with
{ rejectUnauthorized: false }. The default ensures sessions are properly authenticated before they're established.The comment in your script says it's to allow self-signed certificates. Self-signed certificates by themselves don't provide authentication, they're just a pinky promise that the signer is who he says he is.
I assume you're checking the certificate fingerprint as a way to authenticate the client? That's okay-ish from a security viewpoint but consider moving to a self-signed CA certificate that signs your client certificates. That way you're working with rather than around the system. :-)
2: Such approach won't work on TLS 1.3 (which alternatives are there?)
TLSv1.3 has a concept of post-handshake authentication but Node.js doesn't support that yet and might never because it interacts badly with HTTP/2, see RFC 8740.
I believe that ensuring that keeping
socket.authorized == falseby default could solve all of these problems in a simple manner.You're welcome to open a pull request but be ready for some pushback because that's a backwards incompatible change with a lot of potential for ecosystem fallout.
I won't argue you're wrong but keep in mind this only happens because your script turns off authentication with
{ rejectUnauthorized: false }. The default ensures sessions are properly authenticated before they're established.The comment in your script says it's to allow self-signed certificates. Self-signed certificates by themselves don't provide authentication, they're just a pinky promise that the signer is who he says he is.
That's true, I didn't notice that turning
rejectUnauthorized: trueactually enforces the check at each page reload. I keep it false because we want to use a fallback authentication mechanism alongside client certificates.
The comment in the code snippet is outdated, actually I am using a self-signed CA which in turn issued server and client certificates.I guess you might be right about retrocompatibility of such a pull request (even though developers which stuck to the documentation are going to have the expected behaviour, yet I also expect that the ones who figured out the quirk are possibly going to be surprised).
I think a documentation update is likely the best approach, yet I would also issue alongside it an official notice to inform developers who didn't figure this out.
Do you want to open a documentation pull request?
I'm not quite sure what you mean by "official notice" - a note in the release notes?
I'm not quite sure what you mean by "official notice" - a note in the release notes?
Yep, exactly! Sorry I could not find the correct words.
I might consider doing a pull request later on on, everyone is free to make its own though without waiting mine :)Hey guys, I was going a little crazy because of the same issue...
I won't argue you're wrong but keep in mind this only happens because your script turns off authentication with { rejectUnauthorized: false }. The default ensures sessions are properly authenticated before they're established.
Is it possible to implement the same behavior in the handler?
(I tried to look for the default implementation but just couldn't find it)I have a similar issue. However in my case
socket.authorizedturns into atruewhen I send the same request second time even when client certificate is never authenticated. Surely, I am not the only one and it must be a bug. Otherwise, I don't see the benefit of adding such method and like others have said it is very misleading.Edit: I found the issue, I don't know if I still call it a bug. I think the issue depends on both client and server. In my part, if client and server handshake is done at least even once, I could turn off the client, close the connection or even change the client certificate, the client was able to reconnect the server without any problems. I think if the client saves the
previously established sessionin some way then it can always reconnect to the server.I think documentation should be updated like others have suggested. Unfortunately, I saw the same issue in a lot of code snippets and it might lead to security issues if the developers do not realize it.
Would be nice to have a solution for this or at least have it documented. This was unexpected behavior for me.
chrome also triggers this issue. You need to wait for about a minute before the second request
github-actions commented
on Jun 27, 2026 on Jun 27, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jun 27, 2026 Re-measured this on current main before writing the doc text: when the client presented a certificate, the verification result — including the error — carries across resumption on both TLS 1.2 and 1.3 (
SSL_get_verify_resultreads the session-stored result), soauthorizeddoesn't flip totruein that case. The path that still flips is TLS 1.3 with no client certificate at all: full handshakefalse→ resumedtrue, withgetPeerCertificate()empty — the PSK carve-out from #23188. Opened #64584 documenting the caveat as invited, with the practicalisSessionReused()+ peer-cert check forrejectUnauthorized: falseservers.- removedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jul 19, 2026 - Reacted by soreavis
- added a commit that references this issue
on Jul 29, 2026
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
What is the expected behavior?
What do you see instead?
Additional information
(req.socket as tls.TLSSocket).authorizedflag should always be false if the client certificate doesn't match the server CA. In this case it becomes true after a page reload from Firefox for unknown reasons.