Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

One exclamation mark switched off the check: forged alarm acknowledgements in OpenNMS (CVE-2026-19182)

TL;DR What: In OpenNMS, the v2 Alarm REST API checked whether the caller may act as the ackUser they name only when that name was blank. For any real name the check never ran. In my local test on Horizon 36.0.1, an a

TL;DR

  • What: In OpenNMS, the v2 Alarm REST API checked whether the caller may act as the ackUser they name only when that name was blank. For any real name the check never ran. In my local test on Horizon 36.0.1, an account with only ROLE_REST acknowledged an alarm recorded as admin. An account that also had ROLE_READONLY did the same. The v1 API refused both requests.
  • Impact: per the CVE record, a low-privileged authenticated user (ROLE_REST) could acknowledge, escalate or clear alarms recorded as an arbitrary username, and with ROLE_READONLY could change alarm state despite the read-only restriction. Rated Medium 4.3 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N). CWE-863.
  • Fixed in Horizon 36.0.3, and Meridian 2024.3.12 and 2025.0.9. CVE-2026-19182, assigned by OpenNMS, lists me as finder.

Why you should care

OpenNMS Horizon and Meridian are network monitoring platforms. Operators watch alarms in them, and acknowledging an alarm records who took responsibility for it. That record is what a network operations team looks at when it asks who handled an outage and when.

Two things depend on the acknowledgement check: the name in that record, and the read-only role. A read-only user is meant to see alarms, not change them.

The CVE record also notes that OpenNMS's installation instructions say Meridian and Horizon are meant to run inside an organisation's private network, not exposed to the internet. The attacker here is an authenticated user on that network.

The setup

Both REST APIs let a caller acknowledge an alarm and name the user the acknowledgement is recorded for, ackUser. Before acting, they are supposed to call SecurityHelper.assertUserEditCredentials(), at tag opennms-36.0.1-1, lines 49-65:

public static void assertUserEditCredentials(final SecurityContext securityContext, final String ackUser) {
    final String currentUser = securityContext.getUserPrincipal().getName();

    if (securityContext.isUserInRole(Authentication.ROLE_ADMIN)) {
        // admin can do anything
        return;
    }
    if (securityContext.isUserInRole(Authentication.ROLE_READONLY)) {
        // read only is not allowed to edit
        throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN).entity("User '" + currentUser + "', is a read-only user!").type(MediaType.TEXT_PLAIN).build());
    }
    if (securityContext.isUserInRole(Authentication.ROLE_REST) ||
            securityContext.isUserInRole(Authentication.ROLE_USER) ||
            securityContext.isUserInRole(Authentication.ROLE_MOBILE)) {
        if (ackUser.equals(currentUser) || (!ackUser.equals(currentUser) && securityContext.isUserInRole(Authentication.ROLE_DELEGATE))) {
            // ROLE_REST and ROLE_MOBILE are allowed to modify things as long as it's as the
            // same user as they're logging in with, or if they also have ROLE_DELEGATE.
            ...

So a read-only user is refused, and anyone else may act as a different user only with ROLE_DELEGATE.

The bug

The v1 API calls the helper whenever ackUser is set. At the same tag, opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/AlarmRestService.java, line 186:

if (ackUser != null && StringUtils.isNotBlank(ackUser)) {
    SecurityHelper.assertUserEditCredentials(securityContext, ackUser);
}

The v2 API, opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/AlarmRestService.java, lines 212-215:

final String ackUser = ackUserValue == null ? securityContext.getUserPrincipal().getName() : ackUserValue;
if (ackUser != null && !StringUtils.isNotBlank(ackUser)) {
    SecurityHelper.assertUserEditCredentials(securityContext, ackUser);
}

!StringUtils.isNotBlank(ackUser) is true only for a blank name. For admin, or any real username, the condition is false and the helper never runs. The acknowledgement is then recorded under whatever name the caller sent.

The read-only part comes from where the two APIs put their gates. In the running container's Spring Security config, the v1 acknowledge route excludes ROLE_READONLY, while the v2 catch-all for PUT allows ROLE_REST and ROLE_ADMIN and relies on the helper for the rest. With the helper skipped, nothing on the v2 path stopped a read-only user.

The "aha"

The v1 and v2 code differ by one character. With the !, the check ran only when there was nothing to check.

Proof of concept (benign)

I ran the official opennms/horizon:36.0.1 image locally in Docker with PostgreSQL 14 and a single synthetic alarm. No hosted or third-party system was involved. Two test accounts: acct_a with ROLE_REST only, and acct_ro with ROLE_REST and ROLE_READONLY. Neither had ROLE_DELEGATE. The alarm was reset to unacknowledged before each request.

These are the recorded requests and results:

# acct_a (ROLE_REST), v2
PUT /opennms/api/v2/alarms/1    ack=true&ackUser=admin
-> 204 No Content
GET /opennms/api/v2/alarms/1    (as admin)
-> "ackUser": "admin"

# acct_a, same request on v1 (control)
PUT /opennms/rest/alarms/1      ack=true&ackUser=admin
-> 403 "User 'acct_a', is not allowed to perform updates to alarms as user 'admin'"

# acct_ro (ROLE_REST + ROLE_READONLY), v2
PUT /opennms/api/v2/alarms/1    ack=true&ackUser=admin
-> 204 No Content, "ackUser": "admin"

# acct_ro on v1 (control)
-> 403 "User 'acct_ro', is a read-only user!"

The same results came from a manual walkthrough and from a scripted re-run against fresh containers.

Honest boundary: the run covered acknowledgement only. Escalate and clear go through the same handler, and the CVE record lists them, but I did not run them. I tested Horizon 36.0.1 only, not Meridian, and I did not re-run the test on a fixed release.

The fix

OpenNMS fixed it in PR #8755, "NMS-20030: Fix v2 alarm rest api access issue", merged 2026-08-06, and shipped it in Horizon 36.0.3 on 2026-08-12. At that tag, line 213:

if (ackUser != null && StringUtils.isNotBlank(ackUser)) {
    SecurityHelper.assertUserEditCredentials(securityContext, ackUser);
}

The ! is gone, so v2 now runs the same helper as v1. That restores both protections: a read-only user is refused, and acting as someone else needs ROLE_DELEGATE. The 36.0.3 release notes list the issue and credit the report to me.

I checked the other callers of the helper at 36.0.1. v1 AlarmRestService and v2 SituationsRestService both use the positive condition. The inverted form was only in v2 AlarmRestService.

Takeaways

  1. Double negatives in guards deserve a second look. !isNotBlank reads close enough to isNotBlank that it passes review. If a check exists to stop something, say what it stops: if (StringUtils.isNotBlank(ackUser)), or invert the helper's name rather than its result.
  2. When two API versions share a check, compare the call sites. The v1 and v2 handlers had the same intent and the same helper. A diff of the two lines shows the bug at once.
  3. Don't let the route config and the code each assume the other has the read-only rule. v1 blocked read-only users at the route. v2 left it to the helper. Once the helper was skipped, nothing did.

Disclosure timeline (UTC)

  • 2026-07-06: reproduced locally on Horizon 36.0.1 (synthetic data).
  • 2026-07-17: reported privately by email to the OpenNMS security team.
  • 2026-08-06: fix merged (PR #8755).
  • 2026-08-12: Horizon 36.0.3 released with the fix.
  • 2026-08-13: CVE-2026-19182 published by OpenNMS.

Credit / CTA

If you run OpenNMS, upgrade: Horizon 36.0.3 or later, or Meridian 2024.3.12 or 2025.0.9 or later.

If you maintain an API with more than one version, try this: list every call to your permission helpers and check that each condition around them is the same across versions.

CVE-2026-19182 lists me as finder. Comments and corrections welcome.

References

  1. CVE-2026-19182, "OpenNMS v2 Alarm REST API inverted authorization check lets ROLE_REST users acknowledge alarms as any user and bypass read-only", published 2026-08-13 by OpenNMS. https://www.cve.org/CVERecord?id=CVE-2026-19182
  2. Fix PR #8755, "NMS-20030: Fix v2 alarm rest api access issue". https://github.com/OpenNMS/opennms/pull/8755
  3. OpenNMS Horizon 36.0.3 release notes. https://github.com/OpenNMS/opennms/releases/tag/opennms-36.0.3-1
  4. v2 AlarmRestService.java at opennms-36.0.1-1 (before the fix). https://github.com/OpenNMS/opennms/blob/opennms-36.0.1-1/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/AlarmRestService.java
  5. v2 AlarmRestService.java at opennms-36.0.3-1 (after the fix). https://github.com/OpenNMS/opennms/blob/opennms-36.0.3-1/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/AlarmRestService.java
  6. MITRE CWE-863, Incorrect Authorization. https://cwe.mitre.org/data/definitions/863.html

Santosh Kumar Puppala — AI/ML Platform Architect and security researcher (multiple CVEs; creator of Norviq). GitHub: @Santoshkumarpuppala

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.