Skip to content

Commit 89bab66

Browse files
committed
Add strict validation to OpenMetrics 2.0 encoder to reject invalid input
Signed-off-by: David Ashpole <dashpole@google.com>
1 parent 677d544 commit 89bab66

2 files changed

Lines changed: 357 additions & 3 deletions

File tree

expfmt/openmetrics_2_0_create.go

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import (
1919
"fmt"
2020
"io"
2121
"math"
22+
"sort"
2223
"strconv"
24+
"strings"
25+
"unicode/utf8"
2326

2427
dto "github.com/prometheus/client_model/go"
2528
)
@@ -37,6 +40,15 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
3740
if name == "" {
3841
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
3942
}
43+
if !utf8.ValidString(name) || strings.ContainsAny(name, "\n\r") {
44+
return 0, fmt.Errorf("MetricFamily name %q is not valid UTF-8 or contains raw newlines", name)
45+
}
46+
if in.Help != nil && !utf8.ValidString(*in.Help) {
47+
return 0, fmt.Errorf("MetricFamily help %q is not valid UTF-8", *in.Help)
48+
}
49+
if in.Unit != nil && !utf8.ValidString(*in.Unit) {
50+
return 0, fmt.Errorf("MetricFamily unit %q is not valid UTF-8", *in.Unit)
51+
}
4052

4153
// Try the interface upgrade. If it doesn't work, we'll use a
4254
// bufio.Writer from the sync.Pool.
@@ -149,12 +161,18 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
149161
}
150162

151163
// Finally the samples, one line for each.
164+
if err := checkDuplicateLabelSets(name, in.Metric); err != nil {
165+
return written, err
166+
}
152167
for _, metric := range in.Metric {
168+
if metric == nil {
169+
return written, fmt.Errorf("expected non-nil metric in MetricFamily %s", name)
170+
}
171+
if err := validateMetric20(name, metricType, metric); err != nil {
172+
return written, err
173+
}
153174
switch metricType {
154175
case dto.MetricType_COUNTER:
155-
if metric.Counter == nil {
156-
return written, fmt.Errorf("expected counter in metric %s %s", name, metric)
157-
}
158176
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Counter.GetValue(), 0, false, metric.Counter.Exemplar)
159177
case dto.MetricType_GAUGE:
160178
if metric.Gauge == nil {
@@ -334,3 +352,138 @@ func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric,
334352
_ = isGauge
335353
return 0, errors.New("histogram not implemented yet")
336354
}
355+
356+
func validateMetric20(name string, metricType dto.MetricType, metric *dto.Metric) error {
357+
if err := validateLabels20(metric.Label); err != nil {
358+
return fmt.Errorf("invalid label in metric %s: %w", name, err)
359+
}
360+
361+
switch metricType {
362+
case dto.MetricType_COUNTER:
363+
if metric.Counter == nil {
364+
return fmt.Errorf("expected counter in metric %s %s", name, metric)
365+
}
366+
val := metric.Counter.GetValue()
367+
if math.IsNaN(val) {
368+
return fmt.Errorf("counter value cannot be NaN in metric %s", name)
369+
}
370+
if val < 0 {
371+
return fmt.Errorf("counter value cannot be negative (%g) in metric %s", val, name)
372+
}
373+
if metric.Counter.CreatedTimestamp != nil {
374+
if err := metric.Counter.CreatedTimestamp.CheckValid(); err != nil {
375+
return fmt.Errorf("invalid created timestamp in metric %s: %w", name, err)
376+
}
377+
}
378+
if ex := metric.Counter.Exemplar; ex != nil && ex.Timestamp != nil {
379+
if err := validateExemplar20(ex); err != nil {
380+
return fmt.Errorf("invalid exemplar in metric %s: %w", name, err)
381+
}
382+
}
383+
case dto.MetricType_GAUGE:
384+
if metric.Gauge == nil {
385+
return fmt.Errorf("expected gauge in metric %s %s", name, metric)
386+
}
387+
case dto.MetricType_UNTYPED:
388+
if metric.Untyped == nil {
389+
return fmt.Errorf("expected untyped in metric %s %s", name, metric)
390+
}
391+
}
392+
return nil
393+
}
394+
395+
func validateLabels20(labels []*dto.LabelPair) error {
396+
for i, lp := range labels {
397+
if lp == nil {
398+
return errors.New("expected non-nil label pair")
399+
}
400+
lname := lp.GetName()
401+
if lname == "" {
402+
return errors.New("label name cannot be empty")
403+
}
404+
if !utf8.ValidString(lname) || strings.ContainsAny(lname, "\n\r") {
405+
return fmt.Errorf("label name %q is not valid UTF-8 or contains raw newlines", lname)
406+
}
407+
if !utf8.ValidString(lp.GetValue()) {
408+
return fmt.Errorf("label value %q is not valid UTF-8", lp.GetValue())
409+
}
410+
for j := i + 1; j < len(labels); j++ {
411+
if labels[j] != nil && labels[j].GetName() == lname {
412+
return fmt.Errorf("duplicate label name %q", lname)
413+
}
414+
}
415+
}
416+
return nil
417+
}
418+
419+
func validateExemplar20(e *dto.Exemplar) error {
420+
if err := e.Timestamp.CheckValid(); err != nil {
421+
return err
422+
}
423+
return validateLabels20(e.Label)
424+
}
425+
426+
func equalLabelSets(a, b []*dto.LabelPair) bool {
427+
if len(a) != len(b) {
428+
return false
429+
}
430+
for _, lA := range a {
431+
if lA == nil {
432+
return false
433+
}
434+
found := false
435+
for _, lB := range b {
436+
if lB != nil && lA.GetName() == lB.GetName() && lA.GetValue() == lB.GetValue() {
437+
found = true
438+
break
439+
}
440+
}
441+
if !found {
442+
return false
443+
}
444+
}
445+
return true
446+
}
447+
448+
func checkDuplicateLabelSets(name string, metrics []*dto.Metric) error {
449+
if len(metrics) <= 32 {
450+
for i, metric := range metrics {
451+
if metric == nil {
452+
continue
453+
}
454+
for j := range i {
455+
if metrics[j] != nil && equalLabelSets(metric.Label, metrics[j].Label) {
456+
return fmt.Errorf("duplicate label set in MetricFamily %s", name)
457+
}
458+
}
459+
}
460+
return nil
461+
}
462+
463+
seen := make(map[string]struct{}, len(metrics))
464+
for _, metric := range metrics {
465+
if metric == nil {
466+
continue
467+
}
468+
key := labelSetKey(metric.Label)
469+
if _, ok := seen[key]; ok {
470+
return fmt.Errorf("duplicate label set in MetricFamily %s", name)
471+
}
472+
seen[key] = struct{}{}
473+
}
474+
return nil
475+
}
476+
477+
func labelSetKey(labels []*dto.LabelPair) string {
478+
if len(labels) == 0 {
479+
return ""
480+
}
481+
pairs := make([]string, len(labels))
482+
for i, lp := range labels {
483+
if lp != nil {
484+
pairs[i] = lp.GetName() + "\x00" + lp.GetValue()
485+
}
486+
}
487+
sort.Strings(pairs)
488+
return strings.Join(pairs, "\x00\x00")
489+
}

0 commit comments

Comments
 (0)