diff --git a/commands/getZones.go b/commands/getZones.go index 8cb5513878..81b49f6103 100644 --- a/commands/getZones.go +++ b/commands/getZones.go @@ -484,12 +484,12 @@ func formatDsl(rec *models.RecordConfig, defaultTTL uint32) string { // DnsControl uses the API to get this info. NAMESERVER() is just // to override that when needed. if rec.Name == "@" { - return fmt.Sprintf(`//NAMESERVER("%s")`, rec.AsNS().Ns) + return fmt.Sprintf(`//NAMESERVER(%s)`, jsonQuoted(rec.AsNS().Ns)) } - target = `"` + rec.AsNS().Ns + `"` + target = jsonQuoted(rec.AsNS().Ns) case "MIKROTIK_FORWARDER": // Forwarder: target is dns-servers, metadata has doh_servers/verify_doh_cert - target = `"` + rec.GetRDATA().String() + `"` + target = jsonQuoted(rec.GetRDATA().String()) if rec.Metadata != nil { var fwdParts []string if v := rec.Metadata["doh_servers"]; v != "" { @@ -510,18 +510,18 @@ func formatDsl(rec *models.RecordConfig, defaultTTL uint32) string { target = strings.Join(fj, ", ") } - return fmt.Sprintf(`%s("%s", %s%s%s%s%s%s%s%s)`, rec.Type, rec.Name, target, cfproxy, cfflatten, cfcomment, cftags, mtmeta, hednsDynamic, ttlop) + return fmt.Sprintf(`%s(%s, %s%s%s%s%s%s%s%s)`, rec.Type, jsonQuoted(rec.Name), target, cfproxy, cfflatten, cfcomment, cftags, mtmeta, hednsDynamic, ttlop) } func makeCaa(rec *models.RecordConfig, ttlop string) string { f := rec.AsCAA() var target string if f.Flag == 128 { - target = fmt.Sprintf(`"%s", "%s", CAA_CRITICAL`, f.Tag, f.Value) + target = fmt.Sprintf(`%s, %s, CAA_CRITICAL`, jsonQuoted(f.Tag), jsonQuoted(f.Value)) } else { - target = fmt.Sprintf(`"%s", "%s"`, f.Tag, f.Value) + target = fmt.Sprintf(`%s, %s`, jsonQuoted(f.Tag), jsonQuoted(f.Value)) } - return fmt.Sprintf(`%s("%s", %s%s)`, rec.Type, rec.Name, target, ttlop) + return fmt.Sprintf(`%s(%s, %s%s)`, rec.Type, jsonQuoted(rec.Name), target, ttlop) // TODO(tlim): Generate a CAA_BUILDER() instead? } @@ -529,12 +529,12 @@ func makeCaa(rec *models.RecordConfig, ttlop string) string { func makeR53alias(rec *models.RecordConfig, ttl uint32) string { f := rec.AsR53ALIAS() items := []string{ - `"` + rec.Name + `"`, - `"` + f.AliasType + `"`, - `"` + f.Target + `"`, + jsonQuoted(rec.Name), + jsonQuoted(f.AliasType), + jsonQuoted(f.Target), } if f.ZoneID != "" { - items = append(items, `R53_ZONE("`+f.ZoneID+`")`) + items = append(items, `R53_ZONE(`+jsonQuoted(f.ZoneID)+`)`) } if f.EvalTargetHealth == "true" { items = append(items, "R53_EVALUATE_TARGET_HEALTH(true)") @@ -546,5 +546,5 @@ func makeR53alias(rec *models.RecordConfig, ttl uint32) string { } func makeUknown(rc *models.RecordConfig, ttl uint32) string { - return fmt.Sprintf(`// %s("%s", TTL(%d))`, rc.UnknownTypeName, rc.GetRDATA().String(), ttl) + return fmt.Sprintf(`// %s(%s, TTL(%d))`, strings.NewReplacer("\r", " ", "\n", " ").Replace(rc.UnknownTypeName), jsonQuoted(rc.GetRDATA().String()), ttl) } diff --git a/commands/gz_test.go b/commands/gz_test.go index 15d3a5ccb2..79f3c75480 100644 --- a/commands/gz_test.go +++ b/commands/gz_test.go @@ -3,8 +3,11 @@ package commands import ( "fmt" "os" + "strings" "testing" + "github.com/DNSControl/dnscontrol/v5/models" + "github.com/DNSControl/dnscontrol/v5/pkg/js" _ "github.com/DNSControl/dnscontrol/v5/pkg/providers/_all" "github.com/google/go-cmp/cmp" ) @@ -76,3 +79,48 @@ func testFormat(t *testing.T, domain, format string) { t.Errorf("testFormat mismatch (-got +want):\n%s", diff) } } + +func TestFormatDslEscaping(t *testing.T) { + dc := &models.DomainConfig{Name: "example.com"} + hostile := `x"), A("injected", "192.0.2.66` + + tests := []struct { + name string + label string + rtype string + args []any + }{ + {"label", hostile, "A", []any{"192.0.2.1"}}, + {"ns", "sub", "NS", []any{hostile + ".example.net."}}, + {"apex ns", "@", "NS", []any{"ns1.example.net.\n" + hostile}}, + {"caa", "@", "CAA", []any{0, "issue", hostile}}, + {"caa critical", "@", "CAA", []any{128, "issue", hostile}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc, err := dc.NewRecordConfig(tt.label, 300, tt.rtype, tt.args...) + if err != nil { + t.Fatalf("NewRecordConfig: %v", err) + } + line := formatDsl(rc, 300) + script := fmt.Sprintf("D(\"example.com\", NewRegistrar(\"none\"), DnsProvider(NewDnsProvider(\"none\")),\n%s\n);\n", line) + conf, err := js.ExecuteJavascriptString([]byte(script), false, nil) + if err != nil { + t.Fatalf("generated line does not parse: %v\n%s", err, line) + } + records := conf.Domains[0].Records + if strings.HasPrefix(line, "//") { + if len(records) != 0 { + t.Fatalf("commented-out line produced %d records:\n%s", len(records), line) + } + return + } + if len(records) != 1 { + t.Fatalf("got %d records, want 1:\n%s", len(records), line) + } + if got, want := records[0].GetRDATA().String(), rc.GetRDATA().String(); got != want { + t.Errorf("RDATA = %q, want %q", got, want) + } + }) + } +}