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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions