#!/usr/bin/perl

=head1 NAME

t/sec-CVE-2026-46739.t - Metric injection guard tests

=head1 DESCRIPTION

CVE-2026-46739: metric names and values must not contain control characters
(below ASCII 32), colons (":") or pipes ("|"), as those could be used to forge
extra metric lines in the UDP packet sent to statsd. These tests verify that
such input is rejected with a croak, while legitimate input is accepted.

=cut

use strict;
use warnings;
use Test::More tests => 16;
use Net::Statsd;

# Make sure nothing actually leaves the box: point at a discard port.
$Net::Statsd::HOST = '127.0.0.1';
$Net::Statsd::PORT = 1;

# Run $code, return the exception (or '' if it lived).
sub dies {
    my ($code) = @_;
    my $err = '';
    eval { $code->(); 1 } or $err = $@;
    return $err;
}

# --- Bad metric NAMES (validated centrally in send()) ------------------------

for my $bad (
    [ "test\ncounter" => 'newline'      ],
    [ "test:counter"  => 'colon'        ],
    [ "test|counter"  => 'pipe'         ],
    [ "test\0counter" => 'null byte'    ],
    [ "test\tcounter" => 'tab'          ],
) {
    my ($name, $desc) = @{ $bad };
    like(
        dies(sub { Net::Statsd::increment($name) }),
        qr/malformed metric name/,
        "increment() rejects metric name with $desc",
    );
}

# A direct send() call (the low-level public API) must be guarded too.
like(
    dies(sub { Net::Statsd::send({ "evil\nname" => "1|c" }) }),
    qr/malformed metric name/,
    "direct send() rejects a malformed metric name",
);

# --- Bad metric VALUES (validated at the recording entry points) -------------

like(
    dies(sub { Net::Statsd::gauge('core.temp', "5|c\nevil:9") }),
    qr/malformed metric value/,
    "gauge() rejects a value containing a newline",
);

like(
    dies(sub { Net::Statsd::gauge('core.temp', "5|g") }),
    qr/malformed metric value/,
    "gauge() rejects a value containing a pipe",
);

like(
    dies(sub { Net::Statsd::gauge('core.temp', "5:6") }),
    qr/malformed metric value/,
    "gauge() rejects a value containing a colon",
);

like(
    dies(sub { Net::Statsd::update_stats('some.int', "1|c\nevil:9") }),
    qr/malformed metric value/,
    "update_stats() rejects a malformed delta",
);

# --- Legitimate input must still be accepted (no croak) ----------------------

is(dies(sub { Net::Statsd::increment('good.name') }), '',
    'increment() accepts a normal metric name');

is(dies(sub { Net::Statsd::gauge('core.temp', 55) }), '',
    'gauge() accepts a normal numeric value');

is(dies(sub { Net::Statsd::gauge('core.temp', -18.5) }), '',
    'gauge() accepts a negative float value');

is(dies(sub { Net::Statsd::timing('some.timer', 345) }), '',
    'timing() accepts a normal timing value');

is(dies(sub { Net::Statsd::update_stats('some.int', 10) }), '',
    'update_stats() accepts a normal delta');

is(dies(sub { Net::Statsd::increment(['a.counter', 'b.counter']) }), '',
    'increment() accepts an arrayref of normal names');
