Skip to content

Commit c28218a

Browse files
committed
fix(websocket): prevent prototype pollution in WebSocket options
Replace getIfPropertyExists() with own-property-only access in JSWebSocket.cpp to prevent prototype pollution attacks. Previously, setting Object.prototype.rejectUnauthorized = false would disable TLS verification for all WebSocket connections. Now, only properties directly on the options object are read. - Add getOwnPropertyIfExists() helper to ObjectBindings.h/cpp - Replace all 19 getIfPropertyExists() calls in JSWebSocket.cpp - Uses methodTable()->getOwnPropertySlot() for strictest security
1 parent e4caadf commit c28218a

File tree

3 files changed

+42
-19
lines changed

3 files changed

+42
-19
lines changed

src/bun.js/bindings/ObjectBindings.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,19 @@ JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::VM& vm, JSC::J
9292
return value;
9393
}
9494

95+
JSC::JSValue getOwnPropertyIfExists(JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name)
96+
{
97+
auto& vm = JSC::getVM(globalObject);
98+
auto scope = DECLARE_THROW_SCOPE(vm);
99+
PropertySlot slot(object, PropertySlot::InternalMethodType::GetOwnProperty, nullptr);
100+
if (!object->methodTable()->getOwnPropertySlot(object, globalObject, name, slot)) {
101+
RETURN_IF_EXCEPTION(scope, {});
102+
return JSC::jsUndefined();
103+
}
104+
RETURN_IF_EXCEPTION(scope, {});
105+
JSValue value = slot.getValue(globalObject, name);
106+
RETURN_IF_EXCEPTION(scope, {});
107+
return value;
108+
}
109+
95110
}

src/bun.js/bindings/ObjectBindings.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,11 @@ ALWAYS_INLINE JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::
2424
return getIfPropertyExistsPrototypePollutionMitigation(JSC::getVM(globalObject), globalObject, object, name);
2525
}
2626

27+
/**
28+
* Gets an own property only (no prototype chain lookup).
29+
* Returns jsUndefined() if property doesn't exist as own property.
30+
* This is the strictest form of property access - use for security-critical options.
31+
*/
32+
JSC::JSValue getOwnPropertyIfExists(JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name);
33+
2734
}

src/bun.js/bindings/webcore/JSWebSocket.cpp

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
#include "FetchHeaders.h"
6666
#include "JSFetchHeaders.h"
6767
#include "headers.h"
68+
#include "ObjectBindings.h"
6869

6970
namespace WebCore {
7071
using namespace JSC;
@@ -221,7 +222,7 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
221222

222223
if (JSC::JSObject* options = optionsObjectValue.getObject()) {
223224
const auto& builtinnames = WebCore::builtinNames(vm);
224-
auto headersValue = options->getIfPropertyExists(globalObject, builtinnames.headersPublicName());
225+
auto headersValue = Bun::getOwnPropertyIfExists(globalObject, options, builtinnames.headersPublicName());
225226
RETURN_IF_EXCEPTION(throwScope, {});
226227
if (headersValue) {
227228
if (!headersValue.isUndefinedOrNull()) {
@@ -230,15 +231,15 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
230231
}
231232
}
232233

233-
auto protocolsValue = options->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "protocols"_s)));
234+
auto protocolsValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "protocols"_s)));
234235
RETURN_IF_EXCEPTION(throwScope, {});
235236
if (protocolsValue) {
236237
if (!protocolsValue.isUndefinedOrNull()) {
237238
protocols = convert<IDLSequence<IDLDOMString>>(*lexicalGlobalObject, protocolsValue);
238239
RETURN_IF_EXCEPTION(throwScope, {});
239240
}
240241
} else {
241-
auto protocolValue = options->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "protocol"_s)));
242+
auto protocolValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "protocol"_s)));
242243
RETURN_IF_EXCEPTION(throwScope, {});
243244
if (protocolValue) {
244245
if (!protocolValue.isUndefinedOrNull()) {
@@ -249,12 +250,12 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
249250
}
250251

251252
// Parse TLS options using Zig's SSLConfig.fromJS for full TLS option support
252-
JSValue tlsOptionsValue = options->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "tls"_s)));
253+
JSValue tlsOptionsValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "tls"_s)));
253254
RETURN_IF_EXCEPTION(throwScope, {});
254255
if (tlsOptionsValue && !tlsOptionsValue.isUndefinedOrNull() && tlsOptionsValue.isObject()) {
255256
// Also extract rejectUnauthorized for backwards compatibility
256257
if (JSC::JSObject* tlsOptions = tlsOptionsValue.getObject()) {
257-
auto rejectUnauthorizedValue = tlsOptions->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s)));
258+
auto rejectUnauthorizedValue = Bun::getOwnPropertyIfExists(globalObject, tlsOptions, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s)));
258259
RETURN_IF_EXCEPTION(throwScope, {});
259260
if (rejectUnauthorizedValue && !rejectUnauthorizedValue.isUndefinedOrNull() && rejectUnauthorizedValue.isBoolean()) {
260261
rejectUnauthorized = rejectUnauthorizedValue.asBoolean() ? 1 : 0;
@@ -267,7 +268,7 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
267268
}
268269

269270
// Parse proxy option - can be string or { url, headers }
270-
auto proxyValue = options->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "proxy"_s)));
271+
auto proxyValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "proxy"_s)));
271272
RETURN_IF_EXCEPTION(throwScope, {});
272273
if (proxyValue) {
273274
if (!proxyValue.isUndefinedOrNull()) {
@@ -278,14 +279,14 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
278279
} else if (proxyValue.isObject()) {
279280
// proxy: { url: "http://proxy:8080", headers: {...} }
280281
if (JSC::JSObject* proxyOptions = proxyValue.getObject()) {
281-
auto proxyUrlValue = proxyOptions->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "url"_s)));
282+
auto proxyUrlValue = Bun::getOwnPropertyIfExists(globalObject, proxyOptions, PropertyName(Identifier::fromString(vm, "url"_s)));
282283
RETURN_IF_EXCEPTION(throwScope, {});
283284
if (proxyUrlValue && !proxyUrlValue.isUndefinedOrNull()) {
284285
proxyUrl = convert<IDLUSVString>(*lexicalGlobalObject, proxyUrlValue);
285286
RETURN_IF_EXCEPTION(throwScope, {});
286287
}
287288

288-
auto proxyHeadersValue = proxyOptions->getIfPropertyExists(globalObject, builtinnames.headersPublicName());
289+
auto proxyHeadersValue = Bun::getOwnPropertyIfExists(globalObject, proxyOptions, builtinnames.headersPublicName());
289290
RETURN_IF_EXCEPTION(throwScope, {});
290291
if (proxyHeadersValue && !proxyHeadersValue.isUndefinedOrNull()) {
291292
// Check if it's already a Headers instance (like fetch does)
@@ -312,20 +313,20 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
312313
// Parse agent option - extract proxy from agent.proxy if no explicit proxy
313314
// This supports HttpsProxyAgent and similar agent libraries
314315
if (proxyUrl.isNull() || proxyUrl.isEmpty()) {
315-
auto agentValue = options->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "agent"_s)));
316+
auto agentValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "agent"_s)));
316317
RETURN_IF_EXCEPTION(throwScope, {});
317318
if (agentValue && !agentValue.isUndefinedOrNull() && agentValue.isObject()) {
318319
if (JSC::JSObject* agentObj = agentValue.getObject()) {
319320
// Get agent.proxy (can be URL object or string)
320-
auto agentProxyValue = agentObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "proxy"_s)));
321+
auto agentProxyValue = Bun::getOwnPropertyIfExists(globalObject, agentObj, PropertyName(Identifier::fromString(vm, "proxy"_s)));
321322
RETURN_IF_EXCEPTION(throwScope, {});
322323
if (agentProxyValue && !agentProxyValue.isUndefinedOrNull()) {
323324
if (agentProxyValue.isString()) {
324325
proxyUrl = convert<IDLUSVString>(*lexicalGlobalObject, agentProxyValue);
325326
} else if (agentProxyValue.isObject()) {
326327
// URL object - get .href property
327328
if (JSC::JSObject* urlObj = agentProxyValue.getObject()) {
328-
auto hrefValue = urlObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "href"_s)));
329+
auto hrefValue = Bun::getOwnPropertyIfExists(globalObject, urlObj, PropertyName(Identifier::fromString(vm, "href"_s)));
329330
RETURN_IF_EXCEPTION(throwScope, {});
330331
if (hrefValue && hrefValue.isString()) {
331332
proxyUrl = convert<IDLUSVString>(*lexicalGlobalObject, hrefValue);
@@ -336,7 +337,7 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
336337
}
337338

338339
// Get agent.proxyHeaders
339-
auto proxyHeadersValue = agentObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "proxyHeaders"_s)));
340+
auto proxyHeadersValue = Bun::getOwnPropertyIfExists(globalObject, agentObj, PropertyName(Identifier::fromString(vm, "proxyHeaders"_s)));
340341
RETURN_IF_EXCEPTION(throwScope, {});
341342
if (proxyHeadersValue && !proxyHeadersValue.isUndefinedOrNull()) {
342343
// If it's a function, call it
@@ -368,16 +369,16 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
368369
// We build a filtered object with only supported TLS options (ca, cert, key, passphrase, rejectUnauthorized)
369370
// to avoid passing invalid properties like ALPNProtocols to the SSL parser
370371
if (rejectUnauthorized == -1 && !sslConfig) {
371-
auto connectOptsValue = agentObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "connectOpts"_s)));
372+
auto connectOptsValue = Bun::getOwnPropertyIfExists(globalObject, agentObj, PropertyName(Identifier::fromString(vm, "connectOpts"_s)));
372373
RETURN_IF_EXCEPTION(throwScope, {});
373374
if (!connectOptsValue || connectOptsValue.isUndefinedOrNull()) {
374-
connectOptsValue = agentObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "options"_s)));
375+
connectOptsValue = Bun::getOwnPropertyIfExists(globalObject, agentObj, PropertyName(Identifier::fromString(vm, "options"_s)));
375376
RETURN_IF_EXCEPTION(throwScope, {});
376377
}
377378
if (connectOptsValue && !connectOptsValue.isUndefinedOrNull() && connectOptsValue.isObject()) {
378379
if (JSC::JSObject* connectOptsObj = connectOptsValue.getObject()) {
379380
// Extract rejectUnauthorized
380-
auto rejectValue = connectOptsObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s)));
381+
auto rejectValue = Bun::getOwnPropertyIfExists(globalObject, connectOptsObj, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s)));
381382
RETURN_IF_EXCEPTION(throwScope, {});
382383
if (rejectValue && rejectValue.isBoolean()) {
383384
rejectUnauthorized = rejectValue.asBoolean() ? 1 : 0;
@@ -387,28 +388,28 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
387388
JSC::JSObject* filteredTlsOpts = JSC::constructEmptyObject(globalObject);
388389
bool hasTlsOpts = false;
389390

390-
auto caValue = connectOptsObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "ca"_s)));
391+
auto caValue = Bun::getOwnPropertyIfExists(globalObject, connectOptsObj, PropertyName(Identifier::fromString(vm, "ca"_s)));
391392
RETURN_IF_EXCEPTION(throwScope, {});
392393
if (caValue && !caValue.isUndefinedOrNull()) {
393394
filteredTlsOpts->putDirect(vm, Identifier::fromString(vm, "ca"_s), caValue);
394395
hasTlsOpts = true;
395396
}
396397

397-
auto certValue = connectOptsObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "cert"_s)));
398+
auto certValue = Bun::getOwnPropertyIfExists(globalObject, connectOptsObj, PropertyName(Identifier::fromString(vm, "cert"_s)));
398399
RETURN_IF_EXCEPTION(throwScope, {});
399400
if (certValue && !certValue.isUndefinedOrNull()) {
400401
filteredTlsOpts->putDirect(vm, Identifier::fromString(vm, "cert"_s), certValue);
401402
hasTlsOpts = true;
402403
}
403404

404-
auto keyValue = connectOptsObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "key"_s)));
405+
auto keyValue = Bun::getOwnPropertyIfExists(globalObject, connectOptsObj, PropertyName(Identifier::fromString(vm, "key"_s)));
405406
RETURN_IF_EXCEPTION(throwScope, {});
406407
if (keyValue && !keyValue.isUndefinedOrNull()) {
407408
filteredTlsOpts->putDirect(vm, Identifier::fromString(vm, "key"_s), keyValue);
408409
hasTlsOpts = true;
409410
}
410411

411-
auto passphraseValue = connectOptsObj->getIfPropertyExists(globalObject, PropertyName(Identifier::fromString(vm, "passphrase"_s)));
412+
auto passphraseValue = Bun::getOwnPropertyIfExists(globalObject, connectOptsObj, PropertyName(Identifier::fromString(vm, "passphrase"_s)));
412413
RETURN_IF_EXCEPTION(throwScope, {});
413414
if (passphraseValue && !passphraseValue.isUndefinedOrNull()) {
414415
filteredTlsOpts->putDirect(vm, Identifier::fromString(vm, "passphrase"_s), passphraseValue);

0 commit comments

Comments
 (0)