fix(sql): reject null bytes in connection parameters to prevent protocol injection (#26952)

## Summary

- Reject null bytes in `username`, `password`, `database`, and `path`
connection parameters for both PostgreSQL and MySQL to prevent wire
protocol parameter injection
- Both the Postgres and MySQL wire protocols use null-terminated strings
in their startup/handshake messages, so embedded null bytes in these
fields act as field terminators, allowing injection of arbitrary
protocol parameters (e.g. `search_path` for schema hijacking)
- The fix validates these fields immediately after UTF-8 conversion and
throws `InvalidArguments` error with a clear message if null bytes are
found

## Test plan

- [x] New test
`test/regression/issue/postgres-null-byte-injection.test.ts` verifies:
- Null bytes in username are rejected with an error before any data is
sent
- Null bytes in database are rejected with an error before any data is
sent
- Null bytes in password are rejected with an error before any data is
sent
  - Normal connections without null bytes still work correctly
- [x] Test verified to fail with `USE_SYSTEM_BUN=1` (unfixed bun) and
pass with `bun bd test` (fixed build)
- [x] Existing SQL tests pass (`adapter-env-var-precedence.test.ts`,
`postgres-stringbuilder-assertion-aggressive.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
robobun
2026-02-12 00:27:00 -08:00
committed by GitHub
parent 993be3f931
commit 44541eb574
3 changed files with 214 additions and 0 deletions

View File

@@ -680,6 +680,20 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
break :brk b.allocatedSlice();
};
// Reject null bytes in connection parameters to prevent Postgres startup
// message parameter injection (null bytes act as field terminators in the
// wire protocol's key\0value\0 format).
inline for (.{ .{ username, "username" }, .{ password, "password" }, .{ database, "database" }, .{ path, "path" } }) |entry| {
if (entry[0].len > 0 and std.mem.indexOfScalar(u8, entry[0], 0) != null) {
bun.default_allocator.free(options_buf);
tls_config.deinit();
if (tls_ctx) |tls| {
tls.deinit(true);
}
return globalObject.throwInvalidArguments(entry[1] ++ " must not contain null bytes", .{});
}
}
const on_connect = arguments[9];
const on_close = arguments[10];
const idle_timeout = arguments[11].toInt32();