From 2674eb200dec358914ba10e7dc0ee0f41d8f954c Mon Sep 17 00:00:00 2001 From: Tom Ron Date: Thu, 8 Oct 2026 09:13:50 +0300 Subject: [PATCH] fix(dns): enforce the configured OVH zone on record operations Co-Authored-By: Claude Sonnet 5.5 --- apps/dokploy/__test__/dns/ovh.test.ts | 40 +++++++++++++++++++ .../settings/dns/handle-dns-provider.tsx | 9 +++-- packages/server/src/utils/dns/ovh.ts | 12 ++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/dns/ovh.test.ts b/apps/dokploy/__test__/dns/ovh.test.ts index 38b968add..f41735682 100644 --- a/apps/dokploy/__test__/dns/ovh.test.ts +++ b/apps/dokploy/__test__/dns/ovh.test.ts @@ -600,3 +600,43 @@ describe("ovhClient.testConnection", () => { expect(calls[0]?.[0]).toBe(`${cfg.baseUrl}/domain/zone/example.com`); }); }); + +describe("ovhClient with a configured zone", () => { + const record = { + type: "A" as const, + name: "app.other.org", + content: "1.2.3.4", + }; + + it("refuses record operations on any other zone without calling OVH", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(); + + const message = /restricted to the zone "example.com"/; + await expect(ovhClient.listRecords(cfg, "other.org")).rejects.toThrow( + message, + ); + await expect( + ovhClient.upsertRecord(cfg, { ...record, zoneId: "other.org" }), + ).rejects.toThrow(message); + await expect( + ovhClient.updateRecord(cfg, "other.org", "1", record), + ).rejects.toThrow(message); + await expect(ovhClient.deleteRecord(cfg, "other.org", "1")).rejects.toThrow( + message, + ); + expect(apiCalls()).toHaveLength(0); + }); + + it("still allows operations on the configured zone", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(ovhSuccess(null), ovhSuccess(null)); + + await expect( + ovhClient.deleteRecord(cfg, "example.com", "1"), + ).resolves.toBeUndefined(); + expect(apiCalls()[0]?.[0]).toBe( + `${cfg.baseUrl}/domain/zone/example.com/record/1`, + ); + }); +}); diff --git a/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx b/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx index 0201999e8..66fdddc69 100644 --- a/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx @@ -559,9 +559,12 @@ export const HandleDnsProvider = ({ dnsProviderId }: Props) => { - Restrict this provider to a single DNS zone. Leave empty - to use every zone the credentials can access. When set, - the GET /domain/zone right is not needed. + Restrict this provider to a single DNS zone: Dokploy + will only list and change records in it. Leave empty to + use every zone the credentials can access. When set, the{" "} + GET /domain/zone right is not needed. This + is enforced by Dokploy only, so also limit the OVH token + to that zone if you need a hard guarantee. diff --git a/packages/server/src/utils/dns/ovh.ts b/packages/server/src/utils/dns/ovh.ts index 8106d3e31..c5a02acc9 100644 --- a/packages/server/src/utils/dns/ovh.ts +++ b/packages/server/src/utils/dns/ovh.ts @@ -252,6 +252,14 @@ const listZoneNames = async (config: OvhConfig) => { } }; +const assertZoneAllowed = (config: OvhConfig, zoneId: string) => { + if (config.zone && zoneId !== config.zone) { + throw new Error( + `OVH: this provider is restricted to the zone "${config.zone}", not "${zoneId}"`, + ); + } +}; + export const ovhClient: DnsClient = { async listZones(config) { if (config.zone) { @@ -262,6 +270,7 @@ export const ovhClient: DnsClient = { }, async listRecords(config, zoneId) { + assertZoneAllowed(config, zoneId); const zone = encodeURIComponent(zoneId); // The listing endpoint only returns ids, so each record is fetched on its own. const ids = await ovhFetch(config, `/domain/zone/${zone}/record`); @@ -279,6 +288,7 @@ export const ovhClient: DnsClient = { }, async upsertRecord(config, record) { + assertZoneAllowed(config, record.zoneId); const zone = encodeURIComponent(record.zoneId); const subDomain = toSubDomain(record.name, record.zoneId); const existing = await ovhFetch( @@ -322,6 +332,7 @@ export const ovhClient: DnsClient = { }, async updateRecord(config, zoneId, recordId, record) { + assertZoneAllowed(config, zoneId); const zone = encodeURIComponent(zoneId); const existing = await ovhFetch( config, @@ -366,6 +377,7 @@ export const ovhClient: DnsClient = { }, async deleteRecord(config, zoneId, recordId) { + assertZoneAllowed(config, zoneId); await ovhFetch( config, `/domain/zone/${encodeURIComponent(zoneId)}/record/${recordId}`,