fix: decode Route53 octal escapes in record names (#5553)

* fix: decode Route53 octal escapes in record names

* fix: keep escaped dots and backslashes in Route53 record names
This commit is contained in:
Souvik Kumar
2026-10-16 21:10:45 +05:30
committed by GitHub
parent 49d250aaa9
commit e0c8d80cde
2 changed files with 79 additions and 3 deletions
+66
View File
@@ -111,6 +111,54 @@ describe("route53Client.listRecords", () => {
]);
});
it("decodes octal escapes such as \\052 for wildcard names", async () => {
send.mockResolvedValueOnce({
ResourceRecordSets: [
{
Name: "\\052.example.com.",
Type: "A",
TTL: 300,
ResourceRecords: [{ Value: "1.2.3.4" }],
},
],
IsTruncated: false,
});
const records = await route53Client.listRecords(config, "Z123");
expect(records[0]).toMatchObject({
id: "A:*.example.com",
name: "*.example.com",
});
});
it("keeps escaped dots so they stay distinct from label separators", async () => {
send.mockResolvedValueOnce({
ResourceRecordSets: [
{
Name: "a\\056b.example.com.",
Type: "A",
TTL: 300,
ResourceRecords: [{ Value: "1.2.3.4" }],
},
{
Name: "a.b.example.com.",
Type: "A",
TTL: 300,
ResourceRecords: [{ Value: "5.6.7.8" }],
},
],
IsTruncated: false,
});
const records = await route53Client.listRecords(config, "Z123");
expect(records.map((r) => r.id)).toEqual([
"A:a\\056b.example.com",
"A:a.b.example.com",
]);
});
it("skips record sets with no ResourceRecords (e.g. alias targets)", async () => {
send.mockResolvedValueOnce({
ResourceRecordSets: [
@@ -355,6 +403,24 @@ describe("route53Client.deleteRecord", () => {
]);
});
it("finds a wildcard record set that Route53 returns escaped", async () => {
const existingSet = {
Name: "\\052.example.com.",
Type: "A",
TTL: 300,
ResourceRecords: [{ Value: "1.2.3.4" }],
};
send.mockResolvedValueOnce({ ResourceRecordSets: [existingSet] });
send.mockResolvedValueOnce({});
await route53Client.deleteRecord(config, "Z123", "A:*.example.com");
const changeCommand = send.mock.calls[1]?.[0] as HasInput;
expect(changeCommand.input.ChangeBatch.Changes).toEqual([
{ Action: "DELETE", ResourceRecordSet: existingSet },
]);
});
it("throws when the record no longer exists", async () => {
send.mockResolvedValueOnce({ ResourceRecordSets: [] });
+13 -3
View File
@@ -27,6 +27,14 @@ const stripTrailingDot = (name: string) => name.replace(/\.$/, "");
const ensureTrailingDot = (name: string) =>
name.endsWith(".") ? name : `${name}.`;
const stripZonePrefix = (id: string) => id.replace(/^\/hostedzone\//, "");
// Route53 returns special characters in names as \ + three-digit octal code (e.g. * as \052).
// Escaped dots (\056) and backslashes (\134) stay escaped so decoded names remain unique.
const decodeRecordName = (name: string) =>
name.replace(/\\([0-7]{3})/g, (match, code) =>
code === "056" || code === "134"
? match
: String.fromCharCode(Number.parseInt(code, 8)),
);
const buildRecordId = (type: string, name: string) =>
`${type}:${stripTrailingDot(name)}`;
@@ -61,7 +69,8 @@ const findExactRecordSet = async (
if (
candidate &&
candidate.Type === type &&
stripTrailingDot(candidate.Name ?? "") === stripTrailingDot(name)
stripTrailingDot(decodeRecordName(candidate.Name ?? "")) ===
stripTrailingDot(name)
) {
return candidate;
}
@@ -132,10 +141,11 @@ export const route53Client: DnsClient<Route53Config> = {
if (!set.Name || !set.Type || !set.ResourceRecords?.length) {
continue;
}
const name = decodeRecordName(set.Name);
records.push({
id: buildRecordId(set.Type, set.Name),
id: buildRecordId(set.Type, name),
type: set.Type,
name: stripTrailingDot(set.Name),
name: stripTrailingDot(name),
content: set.ResourceRecords.map((r) => r.Value).join("\n"),
ttl: set.TTL ?? 300,
});