From faf06b744e1648eb9a72e83f201b083331064b8a Mon Sep 17 00:00:00 2001 From: Gokhan Kurt Date: Mon, 31 Aug 2026 19:05:12 +0300 Subject: [PATCH] Only add @SECLEVEL=0 where the TLS backend supports it @SECLEVEL is an OpenSSL extension to the cipher-string syntax. Backends that present an OpenSSL-compatible API without implementing it (BoringSSL) reject the entire cipher string with ERR_SSL_INVALID_COMMAND, so every passthrough request with relaxed certificate checks fails at tls.connect. Feature-detected rather than derived from process.versions.openssl, because that value isn't reliable: Bun reports '1.1.0', a real OpenSSL version that does support the directive. --- src/rules/passthrough-handling.ts | 7 ++++--- src/util/openssl-compat.ts | 23 ++++++++++++++++++++++- test/openssl-compat.spec.ts | 24 +++++++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/rules/passthrough-handling.ts b/src/rules/passthrough-handling.ts index 9e56a2a6b..9d6834070 100644 --- a/src/rules/passthrough-handling.ts +++ b/src/rules/passthrough-handling.ts @@ -16,7 +16,7 @@ import { asBuffer } from '../util/buffer-utils'; import { isIP, isLocalhostAddress, normalizeIP } from '../util/ip-utils'; import { CachedDns, dnsLookup, DnsLookupFunction } from '../util/dns'; import { isMockttpBody, encodeBodyBuffer } from '../util/request-utils'; -import { areFFDHECurvesSupported } from '../util/openssl-compat'; +import { areFFDHECurvesSupported, isSecLevelSupported } from '../util/openssl-compat'; import { findRawHeaderIndex, getHeaderValue } from '../util/header-utils'; import { getDefaultPort } from '../util/url'; import { TlsMetadata, TlsClientHello } from '../util/socket-extensions'; @@ -178,8 +178,9 @@ export function getUpstreamTlsOptions({ 'AES256-SHA', // This magic cipher is the very obtuse way that OpenSSL downgrades the overall - // security level to allow various legacy settings, protocols & ciphers: - ...(!strictHttpsChecks + // security level to allow various legacy settings, protocols & ciphers. It's + // OpenSSL-only though - other backends reject the whole cipher string: + ...(!strictHttpsChecks && isSecLevelSupported() ? ['@SECLEVEL=0'] : [] ) diff --git a/src/util/openssl-compat.ts b/src/util/openssl-compat.ts index d0d967097..4e3186705 100644 --- a/src/util/openssl-compat.ts +++ b/src/util/openssl-compat.ts @@ -1,3 +1,4 @@ +import * as tls from 'tls'; import * as semver from 'semver'; export function areFFDHECurvesSupported(opensslVersion: string | undefined) { @@ -23,4 +24,24 @@ export function areFFDHECurvesSupported(opensslVersion: string | undefined) { // we assume that the curves are not supported for safety return false; } -} \ No newline at end of file +} + +let secLevelSupport: boolean | undefined; + +export function isSecLevelSupported() { + // The @SECLEVEL cipher-string directive is an OpenSSL extension. Backends that + // don't implement it (e.g. BoringSSL) reject the entire cipher string with + // ERR_SSL_INVALID_COMMAND, rather than ignoring the unknown directive. + + // Feature-detected, not derived from process.versions.openssl, because that + // isn't reliable: Bun reports '1.1.0', a version that does support @SECLEVEL. + if (secLevelSupport === undefined) { + try { + tls.createSecureContext({ ciphers: 'AES128-SHA:@SECLEVEL=0' }); + secLevelSupport = true; + } catch (e) { + secLevelSupport = false; + } + } + return secLevelSupport; +} diff --git a/test/openssl-compat.spec.ts b/test/openssl-compat.spec.ts index f4d66b0e0..b1fe2cc38 100644 --- a/test/openssl-compat.spec.ts +++ b/test/openssl-compat.spec.ts @@ -1,5 +1,7 @@ import { expect } from 'chai'; -import { areFFDHECurvesSupported } from '../src/util/openssl-compat'; +import * as tls from 'tls'; + +import { areFFDHECurvesSupported, isSecLevelSupported } from '../src/util/openssl-compat'; describe('areFFDHECurvesSupported', () => { it('True only for 3+ versions', () => { @@ -22,3 +24,23 @@ describe('areFFDHECurvesSupported', () => { expect(areFFDHECurvesSupported(undefined)).to.be.false; }); }); + +describe('isSecLevelSupported', () => { + it('Matches whether the TLS backend actually accepts the directive', () => { + // Detected rather than hardcoded, since this suite runs on runtimes with + // different TLS backends. The point is that the helper agrees with reality. + let accepted: boolean; + try { + tls.createSecureContext({ ciphers: 'AES128-SHA:@SECLEVEL=0' }); + accepted = true; + } catch (e) { + accepted = false; + } + + expect(isSecLevelSupported()).to.equal(accepted); + }); + + it('Is stable across calls', () => { + expect(isSecLevelSupported()).to.equal(isSecLevelSupported()); + }); +});