diff --git a/src/lib/adapters/openshell/grpc-sandbox-control.test.ts b/src/lib/adapters/openshell/grpc-sandbox-control.test.ts index 8c134b38f87..32e04644127 100644 --- a/src/lib/adapters/openshell/grpc-sandbox-control.test.ts +++ b/src/lib/adapters/openshell/grpc-sandbox-control.test.ts @@ -4,25 +4,25 @@ import { EventEmitter } from "node:events"; import path from "node:path"; import { + type CallOptions, + loadPackageDefinition, Metadata, Server, ServerCredentials, - type CallOptions, - type sendUnaryData, type ServerUnaryCall, type ServerWritableStream, type ServiceClientConstructor, type ServiceError, + type sendUnaryData, } from "@grpc/grpc-js"; -import { loadPackageDefinition } from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import { describe, expect, it, vi } from "vitest"; import { createGrpcOpenShellSandboxControl, createOpenShellGrpcApi, - OpenShellGrpcOutputLimitError, type OpenShellGrpcApi, + OpenShellGrpcOutputLimitError, } from "./grpc-sandbox-control"; class FakeStream extends EventEmitter { @@ -251,7 +251,7 @@ describe("gRPC OpenShell sandbox control", () => { it("rejects sandbox responses without a stable id", async () => { const fake = fakeApi({ sandboxId: "" }); const control = createGrpcOpenShellSandboxControl( - { endpoint: "http://localhost:8080" }, + { endpoint: "http://127.0.0.1:8080" }, fake.api, ); @@ -348,7 +348,11 @@ describe("gRPC OpenShell sandbox control", () => { it.each([ ["ftp://localhost:8080", "must use http:// or https://"], + ["http://localhost:8080", "restricted to loopback"], ["http://gateway.example:8080", "restricted to loopback"], + ["http://128.0.0.1:8080", "restricted to loopback"], + ["http://[::2]:8080", "restricted to loopback"], + ["http://127.999.999.999:8080", "Invalid OpenShell gRPC endpoint"], ["http://localhost:8080/path", "must not contain"], ])("rejects unsafe endpoint %s", (endpoint, message) => { expect(() => createOpenShellGrpcApi({ endpoint })).toThrow(message); @@ -357,13 +361,13 @@ describe("gRPC OpenShell sandbox control", () => { it("rejects bearer tokens or TLS material on plaintext endpoints", () => { expect(() => createOpenShellGrpcApi({ - endpoint: "http://localhost:8080", + endpoint: "http://127.0.0.1:8080", bearerToken: "token", }), ).toThrow("requires TLS"); expect(() => createOpenShellGrpcApi({ - endpoint: "http://localhost:8080", + endpoint: "http://127.0.0.1:8080", caCertificate: Buffer.from("ca"), }), ).toThrow("require an https:// endpoint"); diff --git a/src/lib/adapters/openshell/grpc-sandbox-control.ts b/src/lib/adapters/openshell/grpc-sandbox-control.ts index 63addd72717..246efc2382d 100644 --- a/src/lib/adapters/openshell/grpc-sandbox-control.ts +++ b/src/lib/adapters/openshell/grpc-sandbox-control.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { EventEmitter } from "node:events"; +import { isIP } from "node:net"; import path from "node:path"; import { StringDecoder } from "node:string_decoder"; @@ -74,8 +75,9 @@ export class OpenShellGrpcOutputLimitError extends Error { function isLoopback(hostname: string): boolean { const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase(); + const version = isIP(normalized); return ( - normalized === "localhost" || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized) + (version === 4 && normalized.startsWith("127.")) || (version === 6 && normalized === "::1") ); }