PATCH /server/{id} accepts and persists nonexistent ddns_profiles IDs for a member-owned server. If another user later creates a DDNS profile with one of those IDs, the DDNS worker resolves the stored ID and dispatches an update using the other user's DDNS profile configuration in the context of the attacker's server.
This is a second-order authorization bypass: direct binding to an existing foreign DDNS profile is correctly denied, but an unresolved future ID can be stored first and later becomes a live cross-user reference.
Confirmed on:
v2.0.148b5e382fe217107c7b777ea9c6b4bc3d2e156202The exact affected version range was not determined.
A normal member who owns a server can prebind one or more future DDNS profile IDs to that server. If another user later creates a DDNS profile with a matching ID, the dashboard DDNS worker can use the victim's DDNS profile/provider configuration for the attacker's server.
In the validated worker path, the dispatched DDNS update combines:
This can result in unauthorized DDNS update attempts using another user's DDNS profile context. The attacker does not need permission to bind the victim profile after it exists.
The following were not validated: credential disclosure, account takeover, or guaranteed external DNS modification across all providers. The credentials remain server-side in the worker path. The downstream DNS impact depends on the victim profile's provider configuration and what that provider account is authorized to update.
PATCH /server/{id}cmd/dashboard/controller/server.goservice/singleton/singleton.goservice/singleton/ddns.goservice/singleton/server.gopkg/ddns/ddns.goThe server update path validates submitted DDNS profile IDs through CheckPermission, but that check only rejects existing objects owned by another user. Nonexistent IDs are skipped.
The updateServer path then persists the submitted raw IDs into DDNSProfilesRaw, along with override domain data. Later, the DDNS worker resolves the stored profile IDs by ID and dispatches provider updates without revalidating that the resolved profiles belong to the server owner.
As a result, an invalid unresolved reference can become a valid cross-user reference after another user creates a DDNS profile with the same global auto-increment ID.
The behavior was validated locally with focused regression tests.
Test file:
cmd/dashboard/controller/ddns_second_order_test.go
Test name:
TestUpdateServerAllowsFutureDDNSProfileBindingThenResolvesVictimProfile
Command:
go test ./cmd/dashboard/controller -run TestUpdateServerAllowsFutureDDNSProfileBindingThenResolvesVictimProfile -count=1
Result:
pass
The test demonstrates:
1.1 does not exist.enable_ddns=true and ddns_profiles=[1].DDNSProfiles=[1].1.1 is correctly denied.Test file:
service/singleton/ddns_worker_authz_test.go
Test name:
TestUpdateDDNSDispatchesVictimProfileForAttackerServer
Command:
go test ./service/singleton -run TestUpdateDDNSDispatchesVictimProfileForAttackerServer -count=1
Result:
pass
The test proves that the DDNS worker does not merely resolve the victim profile. It dispatches a DDNS update using the victim profile configuration and attacker server context.
Validated assertions include:
12001 owned by user 100198.51.100.44attacker-controlled.exampleThe attack requires predicting or prebinding future DDNS profile IDs. This limits severity, but does not remove the authorization issue.
Evidence supporting practicality:
uint64 GORM primary keys from model/common.go.createDDNS uses a normal DB.Create(&p) flow and returns p.ID.DDNSProfiles is an unbounded []uint64 in model/server_api.go.updateServer.[1,2,3,4].MaxRetries.This makes the issue semi-practical: exploitation depends on future ID prediction or range prebinding, but the unresolved IDs persist and can become active later.
PATCH /server/{id} should reject any submitted DDNS profile ID that does not both:
The DDNS worker should also avoid trusting stored profile IDs without revalidating ownership before provider resolution or dispatch.
PATCH /server/{id} accepts nonexistent DDNS profile IDs and persists them. If another user later creates a DDNS profile with a matching ID, the stored reference resolves to that user's profile and is consumed by the DDNS worker for the attacker's server.
Apply both bind-time and worker-time validation.
At bind time:
At worker time:
Suggested regression tests:
TestUpdateServerRejectsNonexistentDDNSProfileIDsTestUpdateServerRejectsForeignDDNSProfileIDsTestUpdateServerAcceptsOwnedDDNSProfileIDsTestUpdateDDNSSkipsStaleOrForeignStoredDDNSProfilesA direct bind to an existing foreign DDNS profile is already denied, which shows the intended ownership boundary. The issue is that the same boundary can be bypassed by storing a future unresolved ID before the victim profile exists.
The worker later treats the stored ID as trusted and dispatches a DDNS update using the victim profile's provider configuration with attacker server context. This is an authorization issue in a deferred worker path, not merely malformed input.
{
"nvd_published_at": "2026-06-12T22:16:52Z",
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"severity": "MODERATE",
"github_reviewed_at": "2026-06-26T23:02:37Z"
}