修复受限DNS凭据的挑战记录安全清理
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-20 05:08:25 +08:00
parent 62ed09f765
commit 87cd28f30c
2 changed files with 50 additions and 14 deletions

View File

@ -11,6 +11,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
@ -26,6 +27,13 @@ type aliDNSRecord struct {
Value string
}
type aliDNSListedRecord struct {
ID string `json:"RecordId"`
Name string `json:"RR"`
Type string `json:"Type"`
Value string `json:"Value"`
}
// 每次签发独立记录创建结果,清理时只删除本实例创建且内容未被修改的记录。
type aliDNSChallenge struct {
mu sync.Mutex
@ -121,17 +129,11 @@ func (provider *aliDNSChallenge) CleanUp(domain, token, keyAuth string) error {
if !exists {
return nil
}
var current struct {
RecordID string `json:"RecordId"`
Domain string `json:"DomainName"`
Name string `json:"RR"`
Type string `json:"Type"`
Value string `json:"Value"`
}
if err := provider.call("DescribeDomainRecordInfo", url.Values{"RecordId": {created.ID}}, &current); err != nil {
current, err := provider.findCreatedRecord(created)
if err != nil {
return err
}
if current.RecordID != created.ID || current.Domain != provider.zone || current.Name != created.Name || current.Type != "TXT" || current.Value != created.Value {
if current.ID != created.ID || current.Name != created.Name || current.Type != "TXT" || current.Value != created.Value {
return errors.New("DNS 挑战记录已被修改,保留记录并停止自动清理")
}
if err := provider.call("DeleteDomainRecord", url.Values{"RecordId": {created.ID}}, nil); err != nil {
@ -141,6 +143,34 @@ func (provider *aliDNSChallenge) CleanUp(domain, token, keyAuth string) error {
return nil
}
// 使用指定托管区域的记录列表复核,不要求额外的跨区域单记录查询权限。
func (provider *aliDNSChallenge) findCreatedRecord(created aliDNSRecord) (aliDNSListedRecord, error) {
const pageSize = 500
for page := 1; page <= 20; page++ {
var response struct {
Total int `json:"TotalCount"`
Records struct {
Record []aliDNSListedRecord `json:"Record"`
} `json:"DomainRecords"`
}
if err := provider.call("DescribeDomainRecords", url.Values{
"DomainName": {provider.zone}, "RRKeyWord": {created.Name}, "Type": {"TXT"},
"PageSize": {strconv.Itoa(pageSize)}, "PageNumber": {strconv.Itoa(page)},
}, &response); err != nil {
return aliDNSListedRecord{}, err
}
for _, record := range response.Records.Record {
if record.ID == created.ID {
return record, nil
}
}
if page*pageSize >= response.Total || len(response.Records.Record) == 0 {
break
}
}
return aliDNSListedRecord{}, errors.New("无法核对本次 DNS 挑战记录,已停止自动清理")
}
func aliDNSPercentEncode(value string) string {
return strings.ReplaceAll(url.QueryEscape(value), "+", "%20")
}

View File

@ -33,12 +33,18 @@ func TestAutomaticDNSCleansOnlyOwnedUnchangedRecords(t *testing.T) {
id = fmt.Sprint(adds)
records[id] = aliDNSRecord{ID: id, Name: r.Form.Get("RR"), Value: r.Form.Get("Value")}
_ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id})
case "DescribeDomainRecordInfo":
record := records[id]
if tamper {
record.Value = "changed-by-user"
case "DescribeDomainRecords":
if r.Form.Get("DomainName") != "example.com" || r.Form.Get("Type") != "TXT" || r.Form.Get("PageNumber") == "" {
t.Error("record lookup must be scoped to the configured zone")
}
_ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id, "DomainName": "example.com", "RR": record.Name, "Type": "TXT", "Value": record.Value})
listed := make([]aliDNSListedRecord, 0, len(records))
for _, record := range records {
if tamper {
record.Value = "changed-by-user"
}
listed = append(listed, aliDNSListedRecord{ID: record.ID, Name: record.Name, Type: "TXT", Value: record.Value})
}
_ = json.NewEncoder(w).Encode(map[string]any{"TotalCount": len(listed), "DomainRecords": map[string]any{"Record": listed}})
case "DeleteDomainRecord":
if failDelete {
w.WriteHeader(503)