Skip to content

Non-string label values bypass escaping and can produce malformed exposition #791

Description

@milcho0604

Summary

When a label value is not a string, escapeLabelValue() returns it untouched, and it is converted to a string later during interpolation — after escaping would have happened. If that string form contains " or a newline, the rendered exposition is malformed and promtool check metrics rejects the whole response.

Reproduction

const client = require('prom-client');

const registry = new client.Registry();
const g = new client.Gauge({ name: 'g', help: 'h', labelNames: ['x'], registers: [registry] });

// Array label value containing a quote
g.set({ x: ['say "hi"'] }, 1);
console.log(JSON.stringify(await registry.metrics()));

Rendered output (whole body, JSON.stringify'd so the escaping is unambiguous):

"# HELP g h\n# TYPE g gauge\ng{x=\"say \"hi\"\"} 1\n"

The quotes inside the label value are not escaped. With a newline instead, the value breaks the line in two:

g.set({ x: ['a\nb'] }, 1);
// "# HELP g h\n# TYPE g gauge\ng{x=\"a\nb\"} 1\n"

A plain string with the same content is escaped correctly, which is the contrast:

g.set({ x: 'say "hi"' }, 1);
// "# HELP g h\n# TYPE g gauge\ng{x=\"say \\\"hi\\\"\"} 1\n"

Effect on scraping

Rendering a registry that holds one such series plus two ordinary ones and piping it to promtool check metrics:

healthy_metric 1
bad_metric{x="oops"quote"} 1
another_healthy 2
error while linting: text format parsing error in line 7: unexpected end of label value "oops"

promtool exits rc=1. The same body with a plain string label value (bad_metric{x="oops\"quote"} 1) exits rc=0. The whole response is rejected, not just the offending series. (I checked the exposition with promtool; I did not point a running Prometheus server at it.)

Where it comes from

lib/registry.js:

function escapeLabelValue(str) {
	if (typeof str !== 'string') {
		return str;
	}
	return escapeString(str).replace(/"/g, '\\"');
}

The value is interpolated afterwards as `${n}="${escapeLabelValue(v)}"`, so a non-string is stringified after the escaping step has already been skipped. Removing the early return locally (coercing before escaping) changes the first example's output to g{x="say \"hi\""} 1, which isolates the behaviour to those lines.

Note

#194 described the same scraping failure for string label values and was closed as fixed. This is the remaining path for values that are not strings.

Whether non-string label values should be coerced and escaped, or rejected when recorded, seems like a policy call rather than something to decide in a patch — arrays in particular are already used in the wild (#582), so rejecting them would be a behaviour change. Happy to open a PR for whichever direction you prefer.

Environment

  • prom-client: main @ 21c8c85
  • Node.js: v25.9.0
  • promtool 3.11.3 (prom/prometheus@sha256:e4254400b856)
  • Scope of what I tested: Gauge#set() with a label value, rendered via registry.metrics().

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions