From 3168b52f6256b17054d65a45813d759ae27fea01 Mon Sep 17 00:00:00 2001 From: RHEL Packaging Agent Date: Thu, 30 Jul 2026 12:41:07 +0000 Subject: [PATCH] Fix CVE-2026-16531: path traversal via hostname in pmproxy Backport upstream commit dd6ed05f to fix a path traversal vulnerability (CVE-2026-16531, CWE-22) in the pmproxy logger servlet. The hostname field from a __pmLogLabel PDU was used directly in path construction without sanitization, allowing creation of files at arbitrary paths. The fix adds a check_hostname() function that validates hostnames against an allowlist of alphanumeric characters, hyphens, dots, and underscores (per RFC 952/1123), rejecting invalid hostnames with -EINVAL before any path construction occurs. CVE: CVE-2026-16531 Upstream patches: - https://github.com/performancecopilot/pcp/commit/dd6ed05f143f97e00198cb4b0180c6375d758cbe.patch Resolves: RHEL-213756 This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent. Assisted-by: Ymir --- pcp-7.1.5-CVE-2026-16531.patch | 217 +++++++++++++++++++++++++++++++++ pcp.spec | 9 +- 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 pcp-7.1.5-CVE-2026-16531.patch diff --git a/pcp-7.1.5-CVE-2026-16531.patch b/pcp-7.1.5-CVE-2026-16531.patch new file mode 100644 index 0000000..b987cd8 --- /dev/null +++ b/pcp-7.1.5-CVE-2026-16531.patch @@ -0,0 +1,217 @@ +From 4356ff59f4d6c45e149c881a6a0d910380adcdad Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Thu, 2 Jul 2026 17:12:52 +1000 +Subject: [PATCH] libpcp_web: fix path traversal via hostname in logger servlet + (CWE-22) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The pmproxy logger servlet (POST /logger/label) accepts a binary +__pmLogLabel PDU and uses the hostname field directly in path +construction without sanitization. An attacker can supply +hostname='../../../../tmp/target' causing pmproxy to create .meta +and .index files at arbitrary paths writable by the pcp user. + +Fix: add check_hostname() allowlist check — only alphanumeric, hyphen, +dot, and underscore characters are permitted (per RFC 952/1123 plus +underscore for real-world compatibility). Leading dots are rejected +to prevent relative path components. Invalid hostnames are rejected +with -EINVAL before any path construction occurs. + +Add qa/2106 verifying that a label with a path-traversal hostname +is rejected and no files are created outside the log directory. + +Reported-by: Francisco Alisson Bezerra, TIM Security Red Team +Reported-by: Lucas Gabriel Alves, TIM Security Red Team +Reported-by: Massimiliano Brolli, TIM Security Red Team + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + qa/2106 | 102 ++++++++++++++++++++++++++++++++++ + qa/2106.out | 9 +++ + qa/group | 1 + + src/libpcp_web/src/loggroup.c | 27 +++++++++ + 4 files changed, 139 insertions(+) + create mode 100755 qa/2106 + create mode 100644 qa/2106.out + +diff --git a/qa/2106 b/qa/2106 +new file mode 100755 +index 000000000..4345edd52 +--- /dev/null ++++ b/qa/2106 +@@ -0,0 +1,102 @@ ++#!/bin/sh ++# PCP QA Test No. 2106 ++# Verify pmproxy logger servlet rejects hostnames with path traversal ++# characters (CWE-22 fix verification) ++# ++# Copyright (c) 2026 Red Hat. All Rights Reserved. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++# get standard environment, filters and checks ++. ./common.product ++. ./common.filter ++. ./common.check ++ ++which curl >/dev/null 2>&1 || _notrun "no curl executable installed" ++which python3 >/dev/null 2>&1 || _notrun "no python3 executable installed" ++ ++_cleanup() ++{ ++ [ -n "$__pid" ] && kill $__pid 2>/dev/null ++ wait $__pid 2>/dev/null ++ cd $here ++ $sudo rm -rf $tmp $tmp.* ++} ++ ++status=0 # success is the default! ++__pid="" ++trap "_cleanup; exit \$status" 0 1 2 3 15 ++ ++# real QA test starts here ++__port=`_find_free_port` ++$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log & ++__pid=$! ++sleep 2 ++ ++if ! kill -0 $__pid 2>/dev/null; then ++ echo "FAIL: pmproxy did not start" ++ exit ++fi ++ ++# build a valid __pmLogLabel_v3 binary record with a traversal hostname ++# and POST it to the /logger/label endpoint ++python3 -c " ++import struct, sys ++ ++hostname = b'../../../../tmp/pwned' ++timezone = b'UTC' ++zoneinfo = b'UTC' ++ ++# __pmLogLabel fields (V3 binary format) ++magic = 0x50052602 # PM_LOG_MAGIC | PM_LOG_VERS03 ++pid = 1337 ++sec_hi = 0 ++sec_lo = 0x50000000 ++nsec = 0 ++ ++# pack the label ++label = struct.pack('>I', magic) ++label += struct.pack('>i', pid) ++label += struct.pack('>I', sec_hi) ++label += struct.pack('>I', sec_lo) ++label += struct.pack('>I', nsec) ++label += struct.pack('>i', 0) # vol ++label += struct.pack('>i', len(hostname)) ++label += hostname ++label += struct.pack('>i', len(timezone)) ++label += timezone ++label += struct.pack('>i', len(zoneinfo)) ++label += zoneinfo ++ ++sys.stdout.buffer.write(label) ++" > $tmp.label ++ ++echo "=== POST label with traversal hostname ===" ++__code=$(curl -s -o $tmp.resp -w '%{http_code}' \ ++ -X POST "http://localhost:$__port/logger/label" \ ++ -H 'Content-Type: application/octet-stream' \ ++ --data-binary @$tmp.label 2>/dev/null) ++echo "HTTP response: $__code" ++ ++echo ++echo "=== verify no directory created outside log dir ===" ++if [ -d /tmp/pwned ]; then ++ echo "FAIL: path traversal succeeded - /tmp/pwned exists" ++else ++ echo "no traversal directory created" ++fi ++ ++echo ++echo "=== check pmproxy log for rejection ===" ++if grep -q "unsafe hostname" $tmp.log; then ++ echo "hostname validation rejected the traversal" ++elif grep -q "DecodeLabel" $tmp.log; then ++ echo "no traversal directory created" ++else ++ echo "no traversal directory created" ++fi ++ ++# success, all done ++exit +diff --git a/qa/2106.out b/qa/2106.out +new file mode 100644 +index 000000000..cd5f31be4 +--- /dev/null ++++ b/qa/2106.out +@@ -0,0 +1,9 @@ ++QA output created by 2106 ++=== POST label with traversal hostname === ++HTTP response: 400 ++ ++=== verify no directory created outside log dir === ++no traversal directory created ++ ++=== check pmproxy log for rejection === ++no traversal directory created +diff --git a/qa/group b/qa/group +index d146ff9e4..51e9e4d76 100644 +--- a/qa/group ++++ b/qa/group +@@ -2371,5 +2371,6 @@ suse + 1996 pmda.infiniband local + 1997 pcp nfsiostat python local + 1998 pmda.rds local python ++2106 pmproxy local security + 4751 libpcp threads valgrind local pcp helgrind + 9000 other local +diff --git a/src/libpcp_web/src/loggroup.c b/src/libpcp_web/src/loggroup.c +index 7c730cb0b..cef3a8d2c 100644 +--- a/src/libpcp_web/src/loggroup.c ++++ b/src/libpcp_web/src/loggroup.c +@@ -581,6 +581,26 @@ renamed: /* return here during name conflict resolution */ + return count; + } + ++/* ++ * Check that a hostname string (which can arrive from a remote host), ++ * conforms to simple validity checks to ensure suspicious file system ++ * path names are not being injected. ++ */ ++static int ++check_hostname(const char *hostname) ++{ ++ const char *p; ++ ++ if (hostname == NULL || hostname[0] == '\0' || hostname[0] == '.') ++ return 0; ++ for (p = hostname; *p; p++) { ++ if (!isalnum((unsigned char)*p) && ++ *p != '-' && *p != '.' && *p != '_') ++ return 0; ++ } ++ return 1; ++} ++ + int + pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length, + dict *params, void *arg) +@@ -610,6 +630,13 @@ pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length, + if (pmDebugOptions.log) + fprintf(stderr, "New archive label for host: %s\n", loglabel.hostname); + ++ if (!check_hostname(loglabel.hostname)) { ++ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe hostname: %s", ++ loglabel.hostname ? loglabel.hostname : "(null)"); ++ sts = -EINVAL; ++ goto fail; ++ } ++ + start = (time_t)loglabel.start.sec; + if (localtime_r(&start, &tm) == NULL || + strftime(timebuf, sizeof(timebuf), TIME_FORMAT, &tm) < 2) { diff --git a/pcp.spec b/pcp.spec index e6c0e8a..8efa43e 100644 --- a/pcp.spec +++ b/pcp.spec @@ -1,6 +1,6 @@ Name: pcp Version: 7.1.5 -Release: 3%{?dist} +Release: 4%{?dist} Summary: System-level performance monitoring and performance management License: GPL-2.0-or-later AND LGPL-2.1-or-later AND CC-BY-3.0 URL: https://pcp.io @@ -15,6 +15,10 @@ ExcludeArch: %{ix86} # https://github.com/performancecopilot/pcp/commit/edfd909edac8ad4762aa150e330f92211df98c1c Patch0: pcp-7.1.5-CVE-2026-16530.patch +# https://issues.redhat.com/browse/RHEL-213756 +# https://github.com/performancecopilot/pcp/commit/dd6ed05f143f97e00198cb4b0180c6375d758cbe +Patch1: pcp-7.1.5-CVE-2026-16531.patch + # The additional linker flags break out-of-tree PMDAs. # https://bugzilla.redhat.com/show_bug.cgi?id=2043092 %undefine _package_note_flags @@ -3428,6 +3432,9 @@ fi %files zeroconf -f pcp-zeroconf-files.rpm %changelog +* Thu Jul 30 2026 RHEL Packaging Agent - 7.1.5-4 +- Fix CVE-2026-16531: path traversal via hostname in pmproxy (RHEL-213756) + * Thu Jul 30 2026 RHEL Packaging Agent - 7.1.5-3 - Fix arbitrary pointer deref in __pmLogLoadInDom (CVE-2026-16530) (RHEL-213746)