|
4 | 4 | */ |
5 | 5 |
|
6 | 6 | import { describe, expect, it } from "bun:test"; |
7 | | -import { validateConnectionIP, validateLaunchCmd, validateServerIdentifier, validateUsername } from "../security.js"; |
| 7 | +import { |
| 8 | + validateConnectionIP, |
| 9 | + validateLaunchCmd, |
| 10 | + validateMetadataValue, |
| 11 | + validateServerIdentifier, |
| 12 | + validateUsername, |
| 13 | +} from "../security.js"; |
8 | 14 |
|
9 | 15 | describe("validateConnectionIP", () => { |
10 | 16 | describe("valid inputs", () => { |
@@ -274,3 +280,86 @@ describe("validateLaunchCmd", () => { |
274 | 280 | }); |
275 | 281 | }); |
276 | 282 | }); |
| 283 | + |
| 284 | +describe("validateMetadataValue", () => { |
| 285 | + describe("valid inputs", () => { |
| 286 | + it("should accept valid GCP zones", () => { |
| 287 | + expect(() => validateMetadataValue("us-central1-a", "zone")).not.toThrow(); |
| 288 | + expect(() => validateMetadataValue("europe-west1-b", "zone")).not.toThrow(); |
| 289 | + expect(() => validateMetadataValue("asia-east1-c", "zone")).not.toThrow(); |
| 290 | + }); |
| 291 | + |
| 292 | + it("should accept valid project IDs", () => { |
| 293 | + expect(() => validateMetadataValue("my-project-123", "project")).not.toThrow(); |
| 294 | + expect(() => validateMetadataValue("gcp_project.name", "project")).not.toThrow(); |
| 295 | + expect(() => validateMetadataValue("prod-app-42", "project")).not.toThrow(); |
| 296 | + }); |
| 297 | + |
| 298 | + it("should accept alphanumeric values with allowed special characters", () => { |
| 299 | + expect(() => validateMetadataValue("simple", "field")).not.toThrow(); |
| 300 | + expect(() => validateMetadataValue("with.dots", "field")).not.toThrow(); |
| 301 | + expect(() => validateMetadataValue("with_underscores", "field")).not.toThrow(); |
| 302 | + expect(() => validateMetadataValue("with-hyphens", "field")).not.toThrow(); |
| 303 | + expect(() => validateMetadataValue("MixedCase123", "field")).not.toThrow(); |
| 304 | + }); |
| 305 | + |
| 306 | + it("should allow empty string (caller provides defaults)", () => { |
| 307 | + expect(() => validateMetadataValue("", "zone")).not.toThrow(); |
| 308 | + }); |
| 309 | + |
| 310 | + it("should allow whitespace-only string (treated as empty)", () => { |
| 311 | + expect(() => validateMetadataValue(" ", "project")).not.toThrow(); |
| 312 | + }); |
| 313 | + }); |
| 314 | + |
| 315 | + describe("invalid inputs", () => { |
| 316 | + it("should reject values exceeding 128 characters", () => { |
| 317 | + const longValue = "a".repeat(129); |
| 318 | + expect(() => validateMetadataValue(longValue, "zone")).toThrow(/too long/); |
| 319 | + }); |
| 320 | + |
| 321 | + it("should accept values at exactly 128 characters", () => { |
| 322 | + const exactValue = "a".repeat(128); |
| 323 | + expect(() => validateMetadataValue(exactValue, "zone")).not.toThrow(); |
| 324 | + }); |
| 325 | + |
| 326 | + it("should reject command substitution with $()", () => { |
| 327 | + expect(() => validateMetadataValue("$(whoami)", "zone")).toThrow(/Invalid zone/); |
| 328 | + }); |
| 329 | + |
| 330 | + it("should reject backtick command substitution", () => { |
| 331 | + expect(() => validateMetadataValue("`id`", "project")).toThrow(/Invalid project/); |
| 332 | + }); |
| 333 | + |
| 334 | + it("should reject semicolon injection", () => { |
| 335 | + expect(() => validateMetadataValue("zone;rm -rf /", "zone")).toThrow(/Invalid zone/); |
| 336 | + }); |
| 337 | + |
| 338 | + it("should reject pipe injection", () => { |
| 339 | + expect(() => validateMetadataValue("zone|cat /etc/passwd", "project")).toThrow(/Invalid project/); |
| 340 | + }); |
| 341 | + |
| 342 | + it("should reject ampersand chaining", () => { |
| 343 | + expect(() => validateMetadataValue("zone&echo pwned", "zone")).toThrow(/Invalid zone/); |
| 344 | + }); |
| 345 | + |
| 346 | + it("should reject path traversal", () => { |
| 347 | + expect(() => validateMetadataValue("../../../etc/passwd", "zone")).toThrow(/Invalid zone/); |
| 348 | + }); |
| 349 | + |
| 350 | + it("should reject spaces", () => { |
| 351 | + expect(() => validateMetadataValue("us central1", "zone")).toThrow(/Invalid zone/); |
| 352 | + }); |
| 353 | + |
| 354 | + it("should reject quotes", () => { |
| 355 | + expect(() => validateMetadataValue("zone'injection", "field")).toThrow(/Invalid field/); |
| 356 | + expect(() => validateMetadataValue('zone"injection', "field")).toThrow(/Invalid field/); |
| 357 | + }); |
| 358 | + |
| 359 | + it("should include field name in error messages", () => { |
| 360 | + expect(() => validateMetadataValue("$(evil)", "gcp_zone")).toThrow(/Invalid gcp_zone/); |
| 361 | + expect(() => validateMetadataValue("bad;value", "gcp_project")).toThrow(/Invalid gcp_project/); |
| 362 | + expect(() => validateMetadataValue("a".repeat(129), "my_field")).toThrow(/my_field is too long/); |
| 363 | + }); |
| 364 | + }); |
| 365 | +}); |
0 commit comments