nebula-mesh: Host advanced overrides allow YAML injection into agent config.yml
Description
internal/configgen/generator.go:86,108,119 interpolates the operator-supplied ListenHost and TunDevice fields raw into a text/template that produces the agent's config.yml. internal/web/advanced.go:20-35 accepts both with only strings.TrimSpace — no character or shape validation.
Exploit
An operator (or attacker with any operator key, given the cross-tenant CRUD advisory) sets adv_tun_device to:
nebula0
lighthouse:
am_lighthouse: true
hosts: ["10.0.0.1"]
#
The agent fetches the rendered config on its next signed poll. On config reload, it loads the injected YAML keys: the host self-promotes to lighthouse, attracts mesh traffic, or sets am_relay: true to be selected as a relay. The ListenHost field has the same shape.
Affected
All released versions prior to v0.3.2.
## Threat model - Today: operator can compromise their own host's config (trivially allowed if they own the host, but they can also set lighthouse/relay flags that the operator-create form does NOT expose — privilege uplift within their own tenant). - Combined with the critical /api/v1 authz advisory: any operator key can mutate ANOTHER tenant's host overrides and inject YAML there. - Post-fix of the authz advisory: still relevant — the agent unconditionally trusts whatever config the server hands it, so any future operator-impersonation bug re-amplifies this.
Suggested fix
Two options, either acceptable:
1. Input validation in parseAdvancedFromForm (internal/web/advanced.go): - ListenHost: regex ^[A-Za-z0-9.:\[\]_-]+$ (IPv4/IPv6/hostname) - TunDevice: regex ^[A-Za-z0-9_-]{1,15}$ (Linux IFNAMSIZ caps at 15) Reject invalid input with a form-level error; do not write to the host row.
- Safer marshalling: switch
configgen/generator.goto marshal a typed Go struct viagopkg.in/yaml.v3(which escapes correctly) instead oftext/templatestring-concat. Larger change, but eliminates this entire injection class.
Option 2 is preferable long-term. Option 1 is the quick fix.
The unsafe_routes advanced field is already netip.Parse{Prefix,Addr}-validated at enroll.go:226-233 — apply the same validation discipline to the other advanced fields.
Affected products
1Patches
1c1506f7344abfeat(configgen): yaml.v3 typed-struct config marshal (closes #126) (#130)
4 files changed · +368 −189
internal/configgen/advanced_test.go+65 −0 modified@@ -3,6 +3,8 @@ package configgen import ( "strings" "testing" + + "gopkg.in/yaml.v3" ) func TestGenerate_DefaultsWhenNoAdvanced(t *testing.T) { @@ -100,3 +102,66 @@ func TestGenerate_PunchyOverride(t *testing.T) { t.Errorf("expected punch: false override; got:\n%s", out) } } + +// TestGenerate_TunDevice_StructuralBreakCharsAreQuoted is the GHSA-7hp6 +// defense-in-depth assertion (issue #126): even if the upstream TunDevice +// validator were bypassed, yaml.v3 marshaling MUST emit structural-break +// characters as safely-quoted scalars so they cannot escape into YAML +// structure. Each pathological TunDevice value below round-trips through +// yaml.Unmarshal exactly — no new top-level keys, no value corruption. +func TestGenerate_TunDevice_StructuralBreakCharsAreQuoted(t *testing.T) { + cases := []string{ + "nebula0\rinjected: true", // CR — splits lines in plain scalars + "nebula0\ninjected: true", // LF — same + "nebula0\x05ctrl", // ENQ — arbitrary control character + `nebula0"quote`, // double-quote — terminates quoted scalars + "nebula0:colon", // colon — key separator in plain scalars + "nebula0#hash", // hash — comment indicator + "nebula0 trailing-space ", // leading/trailing whitespace stripping + } + + for _, dev := range cases { + t.Run(dev, func(t *testing.T) { + out, err := Generate(GeneratorInput{ + HostName: "h", + NebulaIPs: []string{"10.0.0.1"}, + CACertPath: "/etc/nebula/ca.crt", + CertPath: "/etc/nebula/host.crt", + KeyPath: "/etc/nebula/host.key", + TunDevice: dev, + }) + if err != nil { + t.Fatalf("Generate(%q): %v", dev, err) + } + + var parsed struct { + Tun struct { + Dev string `yaml:"dev"` + } `yaml:"tun"` + } + if err := yaml.Unmarshal(out, &parsed); err != nil { + t.Fatalf("invalid YAML for %q: %v\n%s", dev, err, string(out)) + } + if parsed.Tun.Dev != dev { + t.Errorf("round-trip mismatch for %q:\n got: %q\n output:\n%s", dev, parsed.Tun.Dev, string(out)) + } + + // Also assert no injection happened at the top level — only + // the expected keys exist after unmarshal. + var top map[string]any + if err := yaml.Unmarshal(out, &top); err != nil { + t.Fatalf("invalid YAML (top-level) for %q: %v", dev, err) + } + allowed := map[string]bool{ + "pki": true, "static_host_map": true, "lighthouse": true, + "listen": true, "punchy": true, "tun": true, "relay": true, + "logging": true, "firewall": true, + } + for k := range top { + if !allowed[k] { + t.Errorf("unexpected top-level key %q for TunDevice=%q — possible YAML injection", k, dev) + } + } + }) + } +}
internal/configgen/generator.go+6 −159 modified@@ -1,12 +1,5 @@ package configgen -import ( - "bytes" - "fmt" - "strings" - "text/template" -) - // LighthouseInfo describes a lighthouse node for config generation. type LighthouseInfo struct { NebulaIPs []string @@ -20,7 +13,7 @@ type FirewallRule struct { Group string // "any", "admin", etc. } -// AdvancedUnsafeRoute mirrors models.UnsafeRoute for the template. +// AdvancedUnsafeRoute mirrors models.UnsafeRoute for the generator input. type AdvancedUnsafeRoute struct { Route string Via string @@ -48,158 +41,12 @@ type GeneratorInput struct { TunDevice string UnsafeRoutes []AdvancedUnsafeRoute - // Optional: if set, override the path-based pki section with inline PEM blocks. - // Used for Mobile Nebula clients which import a self-contained YAML config. - // When CACertPEM is non-empty, CertPEM and KeyPEM must also be non-empty; - // the template uses literal-block scalars for all three. When empty, the - // template falls back to CACertPath/CertPath/KeyPath (default behavior). + // Optional inline PEM blocks. When CACertPEM is non-empty, CertPEM and + // KeyPEM must also be non-empty; all three are emitted as literal-block + // scalars in the pki section. When empty, the path-based fields above + // are used instead. Mobile Nebula clients use the inline form since + // they import a self-contained YAML config. CACertPEM string CertPEM string KeyPEM string } - -const configTemplate = `pki: -{{- if .CACertPEM }} - ca: | -{{ .CACertPEM | indent4 }} - cert: | -{{ .CertPEM | indent4 }} - key: | -{{ .KeyPEM | indent4 }} -{{- else }} - ca: {{ .CACertPath }} - cert: {{ .CertPath }} - key: {{ .KeyPath }} -{{- end }} - -{{- if .IsLighthouse }} - -static_host_map: {} - -lighthouse: - am_lighthouse: true - {{- if gt .ListenPort 0 }} - # Lighthouse listens on port {{ .ListenPort }} - {{- end }} - -listen: - host: {{ if .ListenHost }}{{ .ListenHost }}{{ else }}0.0.0.0{{ end }} - port: {{ if gt .ListenPort 0 }}{{ .ListenPort }}{{ else }}4242{{ end }} - -{{- else }} - -static_host_map: - {{- range $lh := .Lighthouses }} - {{- range $lh.NebulaIPs }} - "{{ . }}": ["{{ $lh.PublicAddr }}"] - {{- end }} - {{- end }} - -lighthouse: - am_lighthouse: false - hosts: - {{- range $lh := .Lighthouses }} - {{- range $lh.NebulaIPs }} - - "{{ . }}" - {{- end }} - {{- end }} - -listen: - host: {{ if .ListenHost }}{{ .ListenHost }}{{ else }}0.0.0.0{{ end }} - port: {{ if gt .ListenPort 0 }}{{ .ListenPort }}{{ else }}0{{ end }} - -{{- end }} - -punchy: - punch: {{ if .PunchyOverride }}{{ .PunchyOverride }}{{ else }}true{{ end }} -{{- if or .MTU .TunDevice .UnsafeRoutes }} - -tun: - {{- if .TunDevice }} - dev: {{ .TunDevice }} - {{- end }} - {{- if .MTU }} - mtu: {{ .MTU }} - {{- end }} - {{- if .UnsafeRoutes }} - unsafe_routes: - {{- range .UnsafeRoutes }} - - route: {{ .Route }} - via: {{ .Via }} - {{- end }} - {{- end }} -{{- end }} - -{{- if .IsRelay }} - -relay: - am_relay: true -{{- else if .Relays }} - -relay: - relays: - {{- range .Relays }} - - "{{ . }}" - {{- end }} -{{- end }} - -logging: - level: info - format: text - -firewall: - outbound: - {{- range .FirewallOutbound }} - - port: {{ .Port }} - proto: {{ .Proto }} - {{- if ne .Group "any" }} - group: {{ .Group }} - {{- else }} - host: any - {{- end }} - {{- end }} - - inbound: - {{- range .FirewallInbound }} - - port: {{ .Port }} - proto: {{ .Proto }} - {{- if ne .Group "any" }} - group: {{ .Group }} - {{- else }} - host: any - {{- end }} - {{- end }} -` - -// indentLines indents each line of the given string by n spaces. -// Strips trailing newline to avoid empty indented lines at the end. -func indentLines(s string, n int) string { - pad := strings.Repeat(" ", n) - s = strings.TrimRight(s, "\n") - lines := strings.Split(s, "\n") - for i, line := range lines { - if line != "" { - lines[i] = pad + line - } - } - return strings.Join(lines, "\n") -} - -// Generate produces a Nebula config.yml from the given input. -func Generate(input GeneratorInput) ([]byte, error) { - funcs := template.FuncMap{ - "indent4": func(s string) string { return indentLines(s, 4) }, - } - - tmpl, err := template.New("nebula-config").Funcs(funcs).Parse(configTemplate) - if err != nil { - return nil, fmt.Errorf("parse template: %w", err) - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, input); err != nil { - return nil, fmt.Errorf("execute template: %w", err) - } - - return buf.Bytes(), nil -}
internal/configgen/generator_test.go+99 −30 modified@@ -96,6 +96,58 @@ func TestGenerate_Lighthouse(t *testing.T) { if !strings.Contains(s, "4242") { t.Error("lighthouse should have listen port") } + // Lock down the explicit empty static_host_map. yaml.v3 emits a non-nil + // empty map as `{}`; a future refactor that switches StaticHostMap to + // omitempty (or to *map[…]) would silently drop the key, which downstream + // Nebula agents may rely on. + if !strings.Contains(s, "static_host_map: {}") { + t.Errorf("lighthouse should emit empty static_host_map: {}; got:\n%s", s) + } +} + +// TestGenerate_LighthouseAndRelay covers the configurations where a host is +// both a lighthouse and a relay. The previous template emitted both +// am_lighthouse: true and am_relay: true; the typed-struct impl must too. +func TestGenerate_LighthouseAndRelay(t *testing.T) { + input := GeneratorInput{ + HostName: "lh-relay-1", + NebulaIPs: []string{"192.168.100.1"}, + IsLighthouse: true, + IsRelay: true, + CACertPath: "/etc/nebula/ca.crt", + CertPath: "/etc/nebula/host.crt", + KeyPath: "/etc/nebula/host.key", + ListenPort: 4242, + FirewallInbound: []FirewallRule{ + {Port: "any", Proto: "any", Group: "any"}, + }, + FirewallOutbound: []FirewallRule{ + {Port: "any", Proto: "any", Group: "any"}, + }, + } + + data, err := Generate(input) + if err != nil { + t.Fatalf("Generate: %v", err) + } + + var parsed struct { + Lighthouse struct { + AmLighthouse bool `yaml:"am_lighthouse"` + } `yaml:"lighthouse"` + Relay struct { + AmRelay bool `yaml:"am_relay"` + } `yaml:"relay"` + } + if err := yaml.Unmarshal(data, &parsed); err != nil { + t.Fatalf("invalid YAML: %v\n%s", err, string(data)) + } + if !parsed.Lighthouse.AmLighthouse { + t.Error("lighthouse.am_lighthouse should be true") + } + if !parsed.Relay.AmRelay { + t.Error("relay.am_relay should be true") + } } func TestGenerate_Relay(t *testing.T) { @@ -253,7 +305,20 @@ KEY...KEY...KEY } } -// Task 2.3 tests: multi-address lighthouses +// Task 2.3 tests: multi-address lighthouses. +// +// Originally asserted exact byte sequences (flow-style + quoted IPs). After +// the yaml.v3 typed-struct migration (issue #126), these assert on parsed +// structure instead — block-style emission is fine as long as the semantic +// content matches. + +// parsedConfig is a minimal subset of nebula config for round-trip assertions. +type parsedConfig struct { + StaticHostMap map[string][]string `yaml:"static_host_map"` + Lighthouse struct { + Hosts []string `yaml:"hosts"` + } `yaml:"lighthouse"` +} func TestGenerate_StaticHostMap_PerAddress(t *testing.T) { input := GeneratorInput{ @@ -281,13 +346,16 @@ func TestGenerate_StaticHostMap_PerAddress(t *testing.T) { t.Fatalf("Generate: %v", err) } - s := string(data) - // Should have one static_host_map entry per lighthouse IP - if !strings.Contains(s, `"10.0.0.1": ["1.2.3.4:4242"]`) { - t.Error("missing IPv4 lighthouse in static_host_map") + var parsed parsedConfig + if err := yaml.Unmarshal(data, &parsed); err != nil { + t.Fatalf("invalid YAML: %v\n%s", err, string(data)) + } + + if got := parsed.StaticHostMap["10.0.0.1"]; len(got) != 1 || got[0] != "1.2.3.4:4242" { + t.Errorf("static_host_map[10.0.0.1] = %v, want [1.2.3.4:4242]", got) } - if !strings.Contains(s, `"fd00::1": ["1.2.3.4:4242"]`) { - t.Error("missing IPv6 lighthouse in static_host_map") + if got := parsed.StaticHostMap["fd00::1"]; len(got) != 1 || got[0] != "1.2.3.4:4242" { + t.Errorf("static_host_map[fd00::1] = %v, want [1.2.3.4:4242]", got) } } @@ -317,13 +385,17 @@ func TestGenerate_LighthouseHosts_AllAddresses(t *testing.T) { t.Fatalf("Generate: %v", err) } - s := string(data) - // lighthouse.hosts should contain both addresses - if !strings.Contains(s, `- "10.0.0.1"`) { - t.Error("missing IPv4 lighthouse in lighthouse.hosts") + var parsed parsedConfig + if err := yaml.Unmarshal(data, &parsed); err != nil { + t.Fatalf("invalid YAML: %v\n%s", err, string(data)) } - if !strings.Contains(s, `- "fd00::1"`) { - t.Error("missing IPv6 lighthouse in lighthouse.hosts") + + want := map[string]bool{"10.0.0.1": true, "fd00::1": true} + for _, h := range parsed.Lighthouse.Hosts { + delete(want, h) + } + if len(want) > 0 { + t.Errorf("missing lighthouse.hosts entries: %v (got %v)", want, parsed.Lighthouse.Hosts) } } @@ -354,26 +426,23 @@ func TestGenerate_MultipleLighthousesEachMulti(t *testing.T) { t.Fatalf("Generate: %v", err) } - s := string(data) - // Should have 4 entries in static_host_map (2 lighthouses × 2 IPs each) - if !strings.Contains(s, `"10.0.0.1": ["1.2.3.4:4242"]`) { - t.Error("missing first lighthouse IPv4") - } - if !strings.Contains(s, `"fd00::1": ["1.2.3.4:4242"]`) { - t.Error("missing first lighthouse IPv6") - } - if !strings.Contains(s, `"10.0.0.2": ["5.6.7.8:4242"]`) { - t.Error("missing second lighthouse IPv4") - } - if !strings.Contains(s, `"fd00::2": ["5.6.7.8:4242"]`) { - t.Error("missing second lighthouse IPv6") - } - - // Parse to ensure valid YAML - var parsed map[string]any + var parsed parsedConfig if err := yaml.Unmarshal(data, &parsed); err != nil { t.Fatalf("invalid YAML: %v\n%s", err, string(data)) } + + expected := map[string]string{ + "10.0.0.1": "1.2.3.4:4242", + "fd00::1": "1.2.3.4:4242", + "10.0.0.2": "5.6.7.8:4242", + "fd00::2": "5.6.7.8:4242", + } + for ip, addr := range expected { + got := parsed.StaticHostMap[ip] + if len(got) != 1 || got[0] != addr { + t.Errorf("static_host_map[%s] = %v, want [%s]", ip, got, addr) + } + } } func TestGenerate_InlinePEM_RoundTrip(t *testing.T) {
internal/configgen/marshal.go+198 −0 added@@ -0,0 +1,198 @@ +package configgen + +import ( + "bytes" + "fmt" + + "gopkg.in/yaml.v3" +) + +// nebulaConfig is the internal typed-struct representation of a Nebula agent +// config.yml. It exists so Generate can marshal through gopkg.in/yaml.v3 +// instead of string-interpolating into a text/template. Every value is +// quoted/escaped by yaml.v3 according to YAML spec, so structural-break +// characters (CR/LF, control chars, ", :, #) in fields like TunDevice can +// never escape into the YAML structure regardless of what upstream validators +// allow. Defense-in-depth on top of input validation (GHSA-7hp6, issue #126). +type nebulaConfig struct { + PKI pkiSection `yaml:"pki"` + StaticHostMap map[string][]string `yaml:"static_host_map"` + Lighthouse lighthouseSection `yaml:"lighthouse"` + Listen listenSection `yaml:"listen"` + Punchy punchySection `yaml:"punchy"` + Tun *tunSection `yaml:"tun,omitempty"` + Relay *relaySection `yaml:"relay,omitempty"` + Logging loggingSection `yaml:"logging"` + Firewall firewallSection `yaml:"firewall"` +} + +// pkiSection's fields are typed `any` because inline PEMs marshal as +// literal-block scalars (via literalString.MarshalYAML) while path-based +// values are plain strings. +type pkiSection struct { + CA any `yaml:"ca"` + Cert any `yaml:"cert"` + Key any `yaml:"key"` +} + +// literalString marshals as a YAML literal-block scalar (|). Used for the +// inline PEM blocks so the multi-line content is preserved verbatim. +type literalString string + +func (l literalString) MarshalYAML() (any, error) { + return &yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.LiteralStyle, + Value: string(l), + }, nil +} + +type lighthouseSection struct { + AmLighthouse bool `yaml:"am_lighthouse"` + Hosts []string `yaml:"hosts,omitempty"` +} + +type listenSection struct { + Host string `yaml:"host"` + Port int `yaml:"port"` +} + +type punchySection struct { + Punch bool `yaml:"punch"` +} + +type tunSection struct { + Dev string `yaml:"dev,omitempty"` + MTU int `yaml:"mtu,omitempty"` + UnsafeRoutes []unsafeRoute `yaml:"unsafe_routes,omitempty"` +} + +type unsafeRoute struct { + Route string `yaml:"route"` + Via string `yaml:"via"` +} + +type relaySection struct { + AmRelay bool `yaml:"am_relay,omitempty"` + Relays []string `yaml:"relays,omitempty"` +} + +type loggingSection struct { + Level string `yaml:"level"` + Format string `yaml:"format"` +} + +type firewallSection struct { + Outbound []firewallRule `yaml:"outbound"` + Inbound []firewallRule `yaml:"inbound"` +} + +// firewallRule is constructed with exactly one of {Group, Host} set per rule — +// matching the shape the previous template emitted: `group: <name>` for named +// groups, `host: any` for the catch-all. +type firewallRule struct { + Port string `yaml:"port"` + Proto string `yaml:"proto"` + Group string `yaml:"group,omitempty"` + Host string `yaml:"host,omitempty"` +} + +// Generate produces a Nebula config.yml from the given input by marshaling +// a typed struct through gopkg.in/yaml.v3. Replaces the previous +// text/template-based generator (GHSA-7hp6 follow-up, issue #126). +func Generate(input GeneratorInput) ([]byte, error) { + cfg := buildConfig(input) + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + if err := enc.Encode(cfg); err != nil { + return nil, fmt.Errorf("encode config: %w", err) + } + if err := enc.Close(); err != nil { + return nil, fmt.Errorf("close encoder: %w", err) + } + return buf.Bytes(), nil +} + +func buildConfig(input GeneratorInput) nebulaConfig { + cfg := nebulaConfig{ + Logging: loggingSection{Level: "info", Format: "text"}, + } + + if input.CACertPEM != "" { + cfg.PKI = pkiSection{ + CA: literalString(input.CACertPEM), + Cert: literalString(input.CertPEM), + Key: literalString(input.KeyPEM), + } + } else { + cfg.PKI = pkiSection{ + CA: input.CACertPath, + Cert: input.CertPath, + Key: input.KeyPath, + } + } + + cfg.StaticHostMap = map[string][]string{} + if input.IsLighthouse { + cfg.Lighthouse = lighthouseSection{AmLighthouse: true} + } else { + var hosts []string + for _, lh := range input.Lighthouses { + for _, ip := range lh.NebulaIPs { + cfg.StaticHostMap[ip] = []string{lh.PublicAddr} + hosts = append(hosts, ip) + } + } + cfg.Lighthouse = lighthouseSection{AmLighthouse: false, Hosts: hosts} + } + + listenHost := input.ListenHost + if listenHost == "" { + listenHost = "0.0.0.0" + } + listenPort := input.ListenPort + if listenPort == 0 && input.IsLighthouse { + listenPort = 4242 + } + cfg.Listen = listenSection{Host: listenHost, Port: listenPort} + + punch := true + if input.PunchyOverride != nil { + punch = *input.PunchyOverride + } + cfg.Punchy = punchySection{Punch: punch} + + if input.MTU != 0 || input.TunDevice != "" || len(input.UnsafeRoutes) > 0 { + ts := &tunSection{Dev: input.TunDevice, MTU: input.MTU} + for _, r := range input.UnsafeRoutes { + ts.UnsafeRoutes = append(ts.UnsafeRoutes, unsafeRoute(r)) + } + cfg.Tun = ts + } + + if input.IsRelay { + cfg.Relay = &relaySection{AmRelay: true} + } else if len(input.Relays) > 0 { + cfg.Relay = &relaySection{Relays: input.Relays} + } + + cfg.Firewall.Outbound = mapFirewallRules(input.FirewallOutbound) + cfg.Firewall.Inbound = mapFirewallRules(input.FirewallInbound) + + return cfg +} + +func mapFirewallRules(in []FirewallRule) []firewallRule { + out := make([]firewallRule, 0, len(in)) + for _, r := range in { + fr := firewallRule{Port: r.Port, Proto: r.Proto} + if r.Group == "any" { + fr.Host = "any" + } else { + fr.Group = r.Group + } + out = append(out, fr) + } + return out +}
Vulnerability mechanics
Root cause
"The agent configuration generator interpolated operator-supplied values directly into a text template without sufficient validation, allowing YAML injection."
Attack vector
An operator, or an attacker with operator privileges due to a separate authorization vulnerability [ref_id=3], can set the `adv_tun_device` field with specially crafted YAML. This YAML includes keys like `lighthouse` and `am_lighthouse: true`, causing the agent to self-promote to a lighthouse node upon configuration reload. The `ListenHost` field is susceptible to the same injection, allowing for similar privilege escalation or network manipulation. [ref_id=3, ref_id=4]
Affected code
The vulnerability exists in `internal/configgen/generator.go` where operator-supplied `ListenHost` and `TunDevice` fields are interpolated into a `text/template`. The `internal/web/advanced.go` file accepts these inputs with minimal validation, only performing `strings.TrimSpace` [ref_id=3, ref_id=4]. The patch modifies `internal/configgen/generator.go` and introduces `internal/configgen/marshal.go` to use typed struct marshalling instead of string interpolation [patch_id=5276176].
What the fix does
The fix replaces the `text/template`-based configuration generator with one that marshals a typed Go struct using `gopkg.in/yaml.v3` [patch_id=5276176]. This change ensures that any input values, including those with structural YAML characters, are safely quoted and escaped by the YAML marshaller. This eliminates the possibility of injecting arbitrary YAML keys and structures into the agent's `config.yml`, regardless of upstream input validation [ref_id=1, patch_id=5276176].
Preconditions
- authAttacker must have operator privileges. This can be achieved directly or by exploiting a separate authorization vulnerability allowing cross-tenant host override mutation [ref_id=3, ref_id=4].
- inputAttacker must be able to control the `ListenHost` or `TunDevice` fields when configuring a host.
Generated on Jun 8, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.