diff --git a/.changeset/cold-dolls-applaud.md b/.changeset/cold-dolls-applaud.md new file mode 100644 index 00000000000..3af11fa80c7 --- /dev/null +++ b/.changeset/cold-dolls-applaud.md @@ -0,0 +1,5 @@ +--- +"@smithy/config-resolver": minor +--- + +validate region is hostname component diff --git a/packages/config-resolver/package.json b/packages/config-resolver/package.json index cdad938d9f9..7c6eadc1aa7 100644 --- a/packages/config-resolver/package.json +++ b/packages/config-resolver/package.json @@ -28,6 +28,7 @@ "@smithy/node-config-provider": "workspace:^", "@smithy/types": "workspace:^", "@smithy/util-config-provider": "workspace:^", + "@smithy/util-endpoints": "workspace:^", "@smithy/util-middleware": "workspace:^", "tslib": "^2.6.2" }, diff --git a/packages/config-resolver/src/regionConfig/checkRegion.spec.ts b/packages/config-resolver/src/regionConfig/checkRegion.spec.ts new file mode 100644 index 00000000000..9781ad5fb9c --- /dev/null +++ b/packages/config-resolver/src/regionConfig/checkRegion.spec.ts @@ -0,0 +1,77 @@ +import { isValidHostLabel } from "@smithy/util-endpoints"; +import { describe, expect, test as it, vi } from "vitest"; + +import { checkRegion } from "./checkRegion"; + +describe("checkRegion", () => { + const acceptedRegionExamples = [ + "us-east-1", + "ap-east-1", + "ap-southeast-4", + "ap-northeast-3", + "ap-northeast-1", + "eu-west-2", + "il-central-1", + "mx-central-1", + "eu-isoe-santaclaus-125", + "us-iso-reindeer-3000", + "eusc-de-gingerbread-8000", + "abcd", + "12345", + ]; + + it("does not throw when the region is a valid host label", () => { + for (const region of acceptedRegionExamples) { + expect(() => checkRegion(region)).not.toThrow(); + } + }); + + it("throws when the region is not a valid host label", () => { + for (const region of [ + "us-east-1-", + "a".repeat(64), + "-us-east-1", + "", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + ".", + "[", + "]", + ";", + `'`, + "?", + "/", + "\\", + "|", + "+-*/", + ]) { + expect(() => checkRegion(region)).toThrow( + `Region not accepted: region="${region}" is not a valid hostname component.` + ); + } + }); + + it("caches accepted regions", () => { + const di = { + isValidHostLabel, + }; + for (const region of acceptedRegionExamples) { + expect(() => checkRegion(region, di.isValidHostLabel)).not.toThrow(); + } + vi.spyOn(di, "isValidHostLabel").mockImplementation(isValidHostLabel); + for (const region of acceptedRegionExamples) { + expect(() => checkRegion(region, di.isValidHostLabel)).not.toThrow(); + } + expect(di.isValidHostLabel).toHaveBeenCalledTimes(0); + expect(() => checkRegion("oh-canada", di.isValidHostLabel)).not.toThrow(); + expect(di.isValidHostLabel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/config-resolver/src/regionConfig/checkRegion.ts b/packages/config-resolver/src/regionConfig/checkRegion.ts new file mode 100644 index 00000000000..c8f754270e1 --- /dev/null +++ b/packages/config-resolver/src/regionConfig/checkRegion.ts @@ -0,0 +1,22 @@ +import { isValidHostLabel } from "@smithy/util-endpoints"; + +/** + * @internal + */ +const validRegions = new Set(); + +/** + * Checks whether region can be a host component. + * + * @param region - to check. + * @param check - checking function. + * + * @internal + */ +export const checkRegion = (region: string, check = isValidHostLabel) => { + if (!validRegions.has(region) && !check(region)) { + throw new Error(`Region not accepted: region="${region}" is not a valid hostname component.`); + } else { + validRegions.add(region); + } +}; diff --git a/packages/config-resolver/src/regionConfig/resolveRegionConfig.ts b/packages/config-resolver/src/regionConfig/resolveRegionConfig.ts index 894287fe957..af8ed63e364 100644 --- a/packages/config-resolver/src/regionConfig/resolveRegionConfig.ts +++ b/packages/config-resolver/src/regionConfig/resolveRegionConfig.ts @@ -1,5 +1,6 @@ import type { Provider } from "@smithy/types"; +import { checkRegion } from "./checkRegion"; import { getRealRegion } from "./getRealRegion"; import { isFipsRegion } from "./isFipsRegion"; @@ -47,11 +48,10 @@ export const resolveRegionConfig = (input: T & RegionInputConfig & Previously return Object.assign(input, { region: async () => { - if (typeof region === "string") { - return getRealRegion(region); - } - const providedRegion = await region(); - return getRealRegion(providedRegion); + const providedRegion = typeof region === "function" ? await region() : region; + const realRegion = getRealRegion(providedRegion); + checkRegion(realRegion); + return realRegion; }, useFipsEndpoint: async () => { const providedRegion = typeof region === "string" ? region : await region(); diff --git a/packages/util-endpoints/src/lib/isValidHostLabel.spec.ts b/packages/util-endpoints/src/lib/isValidHostLabel.spec.ts index 2a0663e3bcf..396924cc7e4 100644 --- a/packages/util-endpoints/src/lib/isValidHostLabel.spec.ts +++ b/packages/util-endpoints/src/lib/isValidHostLabel.spec.ts @@ -32,7 +32,7 @@ describe(isValidHostLabel.name, () => { expect(isValidHostLabel(hostLabelToTest, true)).toBe(output); }); - describe("returns false is any subdomain is invalid", () => { + describe("returns false if any subdomain is invalid", () => { const validHostLabel = testCases .filter(([outputEntry]) => outputEntry === true) .map(([, value]) => value) diff --git a/yarn.lock b/yarn.lock index 44de7676aab..d6941dc3027 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2514,6 +2514,7 @@ __metadata: "@smithy/node-config-provider": "workspace:^" "@smithy/types": "workspace:^" "@smithy/util-config-provider": "workspace:^" + "@smithy/util-endpoints": "workspace:^" "@smithy/util-middleware": "workspace:^" concurrently: "npm:7.0.0" downlevel-dts: "npm:0.10.1"