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:
- dd6ed05f14.patch
Resolves: RHEL-213756
This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.
Assisted-by: Ymir
This commit is contained in:
parent
5d3ebc9889
commit
3168b52f62
217
pcp-7.1.5-CVE-2026-16531.patch
Normal file
217
pcp-7.1.5-CVE-2026-16531.patch
Normal file
@ -0,0 +1,217 @@
|
||||
From 4356ff59f4d6c45e149c881a6a0d910380adcdad Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
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) <noreply@anthropic.com>
|
||||
---
|
||||
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) {
|
||||
9
pcp.spec
9
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 <redhat-ymir-agent@redhat.com> - 7.1.5-4
|
||||
- Fix CVE-2026-16531: path traversal via hostname in pmproxy (RHEL-213756)
|
||||
|
||||
* Thu Jul 30 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 7.1.5-3
|
||||
- Fix arbitrary pointer deref in __pmLogLoadInDom (CVE-2026-16530) (RHEL-213746)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user