Skip to content
14 changes: 14 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,16 @@ void us_internal_ssl_set_inline_reject(SSL *ssl) {
SSL_set_verify(ssl, SSL_VERIFY_PEER, us_inline_reject_verify_callback);
}

/* Socket-level form for the clients whose SSL lives on a us_socket_t (fetch,
* postgres, mysql, valkey, WebSocket): same policy, installed before the
* handshake is driven so a rejected chain never sees the client's Certificate
* flight. A client whose TLS runs in SSLWrapper (proxy tunnels, upgraded
* duplexes, named pipes) has no handshake drive here and is not covered. */
void us_socket_set_inline_reject(struct us_socket_t *s) {
if (!s->ssl || s->ssl_is_server || s->ssl_handshake_state == HANDSHAKE_COMPLETED) return;
us_internal_ssl_set_inline_reject(s_ssl(s));
}
Comment thread
robobun marked this conversation as resolved.

/* Drop the strdup'd passphrase. Called as soon as private-key load completes
* (the only consumer of the passwd_cb), so the secret never outlives ctx
* construction and SSL_CTX_free() is sufficient on every later path. Also
Expand Down Expand Up @@ -1988,6 +1998,10 @@ static void ssl_trigger_handshake(struct us_socket_t *s, int success) {
loop_ssl_data->ssl_last_fatal_error[0] = 0;
loop_ssl_data->ssl_last_fatal_error_owner = NULL;
}
/* The peer never derived the keys our Finished would have carried, so a
* graceful close (code 0) must send a bare FIN, not a close_notify it
* cannot read, and must not wait for a reply. Fatal also refuses writes. */
s->ssl_fatal_error = 1;
us_dispatch_handshake(s, 0, us_ssl_socket_verify_error_from_ssl(s_ssl(s)));
/* Nothing else will tear this connection down (the peer is still waiting
* for a Finished that will never come) - close unless JS already did. */
Expand Down
5 changes: 5 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,11 @@ struct us_socket_t *us_socket_tls_feed(us_socket_r s, const char *data, int leng
/* Send ClientHello after adopt_tls. Separate so the caller can repoint the
* ext slot before any dispatch can fire. */
void us_socket_start_tls_handshake(us_socket_r s) nonnull_fn_decl;
/* Client TLS socket whose rejectUnauthorized policy is on: refuse a bad chain
* during the handshake, so the client's own Certificate flight never reaches a
* server that fails verification. Must run before the handshake is driven
* (on_open, or between adopt_tls and start_tls_handshake). No-op otherwise. */
void us_socket_set_inline_reject(us_socket_r s) nonnull_fn_decl;

/* ── Listen ───────────────────────────────────────────────────────────────
* The listener owns: an embedded group for accepted sockets, the SSL_CTX
Expand Down
4 changes: 4 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,10 @@ impl<'a> HTTPClient<'a> {
self.alpn_offer(),
);

if self.flags.reject_unauthorized {
Comment thread
robobun marked this conversation as resolved.
socket.set_inline_reject();
}

if crate::session_cache::eligible(self) {
let want_tunnel = self.http_proxy.is_some() && self.url.is_https();
// SAFETY: `ssl_ptr` is live and pre-handshake (guarded by
Expand Down
9 changes: 2 additions & 7 deletions src/http_jsc/websocket_client/CppWebSocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,9 @@ impl CppWebSocket {
event_loop.exit();
}

/// A field read on the C++ side: no JS runs, so no event-loop entry.
pub(crate) fn reject_unauthorized(&self) -> bool {
// SAFETY: VirtualMachine::get() returns the live current-thread VM;
// event_loop() yields its raw event-loop pointer (live for VM lifetime).
let event_loop = VirtualMachine::get().event_loop_mut();
event_loop.enter();
let result = WebSocket__rejectUnauthorized(self);
event_loop.exit();
result
WebSocket__rejectUnauthorized(self)
}

/// `buffered_data` and `secure` are handed on to the connected client.
Expand Down
6 changes: 6 additions & 0 deletions src/http_jsc/websocket_client/WebSocketUpgradeClient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,12 @@ where
);
}
}
if this
.cpp_websocket()
.is_some_and(|ws| ws.reject_unauthorized())
{
socket.set_inline_reject();
}
}

// If using proxy, set state to proxy_handshake
Expand Down
2 changes: 1 addition & 1 deletion src/install/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ pub(crate) fn parse_with_tag(
// check for absolute windows paths
#[cfg(windows)]
{
if protocol == 1 && strings::starts_with_windows_drive_letter(dependency) {
if protocol == 1 && strings::starts_with_windows_drive_letter_t(dependency) {
return Some(Version {
literal: sliced.value(),
value: Value {
Expand Down
5 changes: 0 additions & 5 deletions src/paths/string_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,6 @@ pub fn path_contains_node_modules_folder(path: &[u8]) -> bool {

pub use crate::is_sep_any as char_is_any_slash;

#[inline(always)]
pub fn starts_with_windows_drive_letter(s: &[u8]) -> bool {
starts_with_windows_drive_letter_t(s)
}

#[inline(always)]
pub fn starts_with_windows_drive_letter_t<T: Ch>(s: &[T]) -> bool {
s.len() > 2 && s[1] == ch(b':') && {
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/valkey_jsc/js_valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,12 @@ impl<const SSL: bool> SocketHandler<SSL> {
}

pub(crate) fn on_open(this: &JSValkeyClient, socket: SocketType<SSL>) -> JsResult<()> {
if SSL {
let client = this.client.get();
if client.tls.reject_unauthorized(client.vm) {
socket.set_inline_reject();
}
}
this.client_mut().socket = Self::socket(socket);
this.client_mut().on_open(Self::socket(socket))
}
Expand Down
7 changes: 6 additions & 1 deletion src/sql_jsc/mysql/MySQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ impl MySQLConnection {
sni,
true, // is_client
false, // request_cert (server-only)
false, // reject_unauthorized (server-only)
false, // reject_unauthorized (server-only; the client policy is set_inline_reject below)
ext_size,
ext_size,
) else {
Expand All @@ -367,6 +367,11 @@ impl MySQLConnection {
let sock = unsafe { &mut *new_socket };
*sock.ext::<Option<core::ptr::NonNull<JSMySQLConnection>>>() =
core::ptr::NonNull::new(js_connection);
if self.tls_config.reject_unauthorized() != 0
&& matches!(self.ssl_mode, SSLMode::VerifyCa | SSLMode::VerifyFull)
{
sock.set_inline_reject();
}
self.socket = Socket::SocketTls(uws::SocketTLS {
socket: uws::InternalSocket::Connected(new_socket),
});
Expand Down
7 changes: 6 additions & 1 deletion src/sql_jsc/postgres/PostgresSQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ impl PostgresSQLConnection {
sni,
true, // is_client
false, // request_cert (server-only)
false, // reject_unauthorized (server-only)
false, // reject_unauthorized (server-only; the client policy is set_inline_reject below)
ext_size,
ext_size,
) else {
Expand All @@ -505,6 +505,11 @@ impl PostgresSQLConnection {
let sock = unsafe { &mut *new_socket };
*sock.ext::<Option<core::ptr::NonNull<PostgresSQLConnection>>>() =
core::ptr::NonNull::new(self.as_ctx_ptr());
if self.tls_config.reject_unauthorized() != 0
&& matches!(self.ssl_mode, SSLMode::VerifyCa | SSLMode::VerifyFull)
{
sock.set_inline_reject();
}
self.socket.set(Socket::SocketTls(uws::SocketTLS {
socket: uws::InternalSocket::Connected(new_socket),
}));
Expand Down
8 changes: 8 additions & 0 deletions src/uws_sys/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,14 @@ impl<const IS_SSL: bool> NewSocketHandler<IS_SSL> {

// ── TLS ─────────────────────────────────────────────────────────────────

/// Refuse a bad server chain during the handshake, before the client
/// certificate goes out. Client-only; call it before the handshake is driven.
Comment thread
robobun marked this conversation as resolved.
pub fn set_inline_reject(&self) {
if let InternalSocket::Connected(s) = self.socket {
sock(s).set_inline_reject();
}
}

/// `SSL*` if this is a TLS socket, else `None`.
#[inline]
pub fn ssl(&self) -> Option<*mut bun_boringssl_sys::SSL> {
Expand Down
7 changes: 7 additions & 0 deletions src/uws_sys/us_socket_t.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ impl us_socket_t {
c::us_socket_start_tls_handshake(self);
}

/// Refuse a bad server chain during the handshake, before the client
/// certificate goes out. No-op on a server socket or after the handshake.
Comment thread
robobun marked this conversation as resolved.
pub fn set_inline_reject(&mut self) {
c::us_socket_set_inline_reject(self);
}

/// Feed bytes that were already read off the wire (e.g. a ClientHello the
/// plain-TCP layer consumed before the upgrade) through the same decrypt
/// path as bytes arriving from the kernel.
Expand Down Expand Up @@ -601,6 +607,7 @@ mod c {
length: i32,
) -> *mut us_socket_t;
pub(super) safe fn us_socket_start_tls_handshake(s: &mut us_socket_t);
pub(super) safe fn us_socket_set_inline_reject(s: &mut us_socket_t);
}
}

Expand Down
Loading
Loading