Skip to content

Commit a2bd1b0

Browse files
authored
add more context and unit tests for EscapePromQLValue func (#823)
Signed-off-by: Mohammed Abdi <mohammed.munir.abdi@ibm.com>
1 parent cec7127 commit a2bd1b0

File tree

4 files changed

+105
-1
lines changed

4 files changed

+105
-1
lines changed

internal/collector/source/prometheus/prometheus_source_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,45 @@ var _ = Describe("PrometheusSource", func() {
146146
Expect(result.Values[0].Value).To(Equal(0.75))
147147
Expect(result.Values[0].Labels["pod"]).To(Equal("test-pod-1"))
148148
})
149+
150+
It("should escape params so PromQL is safe (no injection)", func() {
151+
var capturedQuery string
152+
mockAPI.queryFunc = func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) {
153+
capturedQuery = query
154+
return model.Vector{
155+
&model.Sample{
156+
Metric: model.Metric{"pod": "p1"},
157+
Value: 0.5,
158+
Timestamp: model.TimeFromUnix(time.Now().Unix()),
159+
},
160+
}, nil, nil
161+
}
162+
163+
err := registry.Register(sourcepkg.QueryTemplate{
164+
Name: "injection_test",
165+
Type: sourcepkg.QueryTypePromQL,
166+
Template: `metric{namespace="{{.namespace}}",model_name="{{.modelID}}"}`,
167+
Params: []string{"namespace", "modelID"},
168+
Description: "Test escaping",
169+
})
170+
Expect(err).NotTo(HaveOccurred())
171+
172+
// Params that would break PromQL or inject labels if unescaped
173+
params := map[string]string{
174+
"namespace": `safe-ns`,
175+
"modelID": `x",namespace="other"`,
176+
}
177+
_, err = source.Refresh(ctx, sourcepkg.RefreshSpec{
178+
Queries: []string{"injection_test"},
179+
Params: params,
180+
})
181+
Expect(err).NotTo(HaveOccurred())
182+
Expect(capturedQuery).NotTo(BeEmpty())
183+
// Escaped: quote and backslash in modelID become \"
184+
Expect(capturedQuery).To(ContainSubstring(`model_name="x\",namespace=\"other\""`))
185+
// Should not contain unescaped injection (extra namespace=)
186+
Expect(capturedQuery).NotTo(MatchRegexp(`namespace="other"`))
187+
})
149188
})
150189

151190
Describe("Caching", func() {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package prometheus
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestPrometheus(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Prometheus Source Suite")
13+
}

internal/collector/source/query_template.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,14 @@ func (r *QueryList) List() []string {
143143
// --- Helpers ---
144144

145145
// EscapePromQLValue escapes a value for safe use in PromQL label matchers.
146-
// Prevents injection by escaping backslashes and double quotes.
146+
// It escapes backslashes and double quotes to prevent injection when values
147+
// are substituted into query templates.
148+
//
149+
// Kubernetes naming (e.g. namespace) only allows DNS labels, so namespace
150+
// values cannot contain " or \. Other parameters are not restricted: e.g.
151+
// VariantAutoscaling.Spec.ModelID is user-controlled and has no pattern
152+
// validation, so escaping is required for modelID and any future
153+
// user- or config-driven parameters.
147154
func EscapePromQLValue(value string) string {
148155
// Escape backslashes first (must be done before escaping quotes)
149156
value = backslashPattern.ReplaceAllString(value, `\\`)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package source
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("EscapePromQLValue", func() {
9+
It("returns empty string unchanged", func() {
10+
Expect(EscapePromQLValue("")).To(Equal(""))
11+
})
12+
13+
It("escapes backslashes", func() {
14+
Expect(EscapePromQLValue(`\`)).To(Equal(`\\`))
15+
Expect(EscapePromQLValue(`foo\bar`)).To(Equal(`foo\\bar`))
16+
Expect(EscapePromQLValue(`\\`)).To(Equal(`\\\\`))
17+
})
18+
19+
It("escapes double quotes", func() {
20+
Expect(EscapePromQLValue(`"`)).To(Equal(`\"`))
21+
Expect(EscapePromQLValue(`foo"bar`)).To(Equal(`foo\"bar`))
22+
Expect(EscapePromQLValue(`""`)).To(Equal(`\"\"`))
23+
})
24+
25+
It("escapes backslashes before quotes so order is correct", func() {
26+
// Backslash first, then quote - result is \\ then \"
27+
Expect(EscapePromQLValue(`\""`)).To(Equal(`\\\"\"`))
28+
})
29+
30+
It("leaves safe characters unchanged", func() {
31+
Expect(EscapePromQLValue("test-ns")).To(Equal("test-ns"))
32+
Expect(EscapePromQLValue("my-model-id")).To(Equal("my-model-id"))
33+
Expect(EscapePromQLValue("a1b2c3")).To(Equal("a1b2c3"))
34+
})
35+
36+
It("prevents PromQL label injection by escaping malicious payload", func() {
37+
// If unescaped, this could close the label and inject another: namespace="other"
38+
malicious := `prod",namespace="other"`
39+
escaped := EscapePromQLValue(malicious)
40+
Expect(escaped).To(Equal(`prod\",namespace=\"other\"`))
41+
// The escaped value should be safe to embed in: metric{namespace="<value>"}
42+
// i.e. metric{namespace="prod\",namespace=\"other\""} is one literal label value
43+
Expect(escaped).NotTo(ContainSubstring(`namespace="other`))
44+
})
45+
})

0 commit comments

Comments
 (0)