Files
bun-src/test/js/sql/sql-mysql-tls-plaintext-injection.test.ts
2026-08-27 21:09:14 +00:00

63 lines
2.6 KiB
TypeScript

// Fault-injection test: requires a server that refuses / drops / sends malformed
// frames, which a healthy container will not do on demand. DO NOT COPY THIS
// PATTERN — anything a real server can produce belongs in describeWithContainer.
// All wire-protocol bytes come from test/js/sql/wire-frames.ts; do not inline
// Buffer.alloc frame construction here.
import { SQL } from "bun";
import { expect, mock, test } from "bun:test";
import {
listeningServer,
MYSQL_CLIENT_SSL,
MYSQL_DEFAULT_CAPABILITIES,
mysqlHandshakeV10,
mysqlOkPacket,
} from "./wire-frames";
test("MySQL TLS handshake rejects plaintext packets buffered behind the server greeting", async () => {
// A man-in-the-middle can append forged packets (e.g. an OK packet that marks
// the connection as authenticated) to the same TCP segment as the server
// greeting. Once the handshake negotiates TLS, everything after the greeting
// must arrive over the encrypted channel; bytes already buffered in plaintext
// must not be fed to the auth/command handlers.
const greeting = mysqlHandshakeV10({
serverVersion: "mock-8.0.0",
authPlugin: "caching_sha2_password",
capabilities: MYSQL_DEFAULT_CAPABILITIES | MYSQL_CLIENT_SSL,
});
// A forged OK packet. If the client keeps consuming the plaintext buffer
// after deciding to upgrade to TLS, this marks the connection as
// authenticated without any certificate ever being validated.
const forgedOk = mysqlOkPacket(2);
const { port, server } = await listeningServer(socket => {
// Greeting and the injected packet arrive in a single segment, before the
// client has sent a byte.
socket.write(Buffer.concat([greeting, forgedOk]));
// Whatever the client sends next (SSLRequest, TLS ClientHello, auth
// response), close so a misbehaving client cannot hang waiting for more.
socket.on("data", () => socket.end());
socket.on("error", () => {});
});
const onconnect = mock();
try {
await using sql = new SQL({
url: `mysql://root:[email protected]:${port}/db`,
max: 1,
tls: { rejectUnauthorized: false },
onconnect,
});
const err = await sql`select 1`.then(
() => ({ code: "UNEXPECTED_SUCCESS" }),
e => ({ code: e?.code ?? String(e) }),
);
// The connection must never be reported as established off the back of a
// plaintext packet, and the buffered bytes must be rejected outright.
expect(onconnect).not.toHaveBeenCalled();
expect(err).toEqual({ code: "ERR_MYSQL_UNEXPECTED_PACKET" });
} finally {
await new Promise<void>(r => server.close(() => r()));
}
});