← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-28799 · CWE-416 · Disclosed 2026-03-06

PJSIP is a free and open source multimedia communication library written in C

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is less a master key and more a pothole on a side road only some VoIP apps even use

CVE-2026-28799 is a heap use-after-free in PJSIP's event subscription framework (evsub.c) affecting versions 2.16 and lower; upstream fixed it in 2.17. The trigger is specific: a presence unsubscription flow using SUBSCRIBE with Expires=0, where callback ordering frees memory and later code still touches it. Upstream says the affected population is applications acting as a presence server (UAS) handling SUBSCRIBE requests, including presence, MWI, and dialog-event support.

The vendor's HIGH 7.5 score is technically defensible in a vacuum because the bug is remote, unauthenticated, and availability-impacting. In real fleets, though, this is a library bug behind an application role requirement: the target has to use PJSIP, expose SIP signaling to the attacker, and actually implement the presence-subscription server path. That sharply reduces exposed population and makes this a downgrade to MEDIUM unless you know you run internet-reachable PBX/UC workloads that use PJSIP presence features.

"Unauthenticated remote crash, but only in PJSIP apps acting as presence servers—not a broad internet-wide fire."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a reachable PJSIP-backed SIP service

The attacker first needs a SIP-speaking application built on vulnerable PJSIP, not just the library sitting dormant on disk. Typical tooling would be sipvicious or plain nmap SIP scripts to identify UDP/TCP 5060/5061 listeners and confirm the stack speaks SIP. This is already narrower than the CVSS vector suggests because PJSIP is an embeddable library used by many client-only or internal-only apps.
Conditions required:
  • Target application embeds PJSIP < 2.17
  • SIP signaling is reachable from the attacker position
  • The service is not fully isolated behind VPN/private voice network boundaries
Where this breaks in practice:
  • Many PJSIP deployments are softphones, SDK integrations, or internal UC components rather than public services
  • Network ACLs, SBCs, VPN requirements, and voice VLAN segmentation commonly reduce exposure
  • A library has no universal network fingerprint, so internet-wide discovery is worse than for branded appliances
Detection/coverage: External scanners can find SIP listeners, but they usually cannot prove this exact library/version remotely. This is primarily an asset inventory/SBOM/package version problem, not a pure network-scan problem.
STEP 02

Reach the presence subscription server path

The bug lives in the event subscription/presence framework, so the attacker must hit an application path that behaves as a presence server (UAS) for SUBSCRIBE. A simple SIP stack that only handles calls or registration is not enough. A practical weaponization tool here is sipp, which can script the exact SUBSCRIBE and Expires=0 sequence described in the advisory and patch notes.
Conditions required:
  • Application supports presence, MWI, or dialog-event subscriptions
  • The vulnerable service accepts attacker-controlled SUBSCRIBE traffic
  • The specific UAS callback flow is enabled in the product build/configuration
Where this breaks in practice:
  • Not all PJSIP-based products enable presence or MWI features
  • Session border controllers, SIP normalization, or upstream auth policy may block or rewrite malformed/unsolicited subscription traffic
  • Some deployments only expose call control while subscription features remain internal or unused
Detection/coverage: SIP transaction logs, SBC telemetry, and application debug logs may show unusual SUBSCRIBE with Expires=0, but generic vuln scanners usually miss this feature-specific reachability question.
STEP 03

Trigger the callback-ordering use-after-free

Per the upstream fix, the vulnerable sequence occurs when on_tsx_state_uas() transitions to TERMINATED, fires pres_on_evsub_state(), frees pool-backed objects, and then on_rx_refresh later references freed memory via pjsip_pres_notify(). The patch fixes this by deferring the terminated-state callback until on_rx_refresh finishes. In practice, the attacker is chasing a reliable crash path more than a clean code-exec primitive.
Conditions required:
  • Runtime follows the vulnerable ordering in evsub.c
  • The crafted unsubscription is processed to the termination path
  • Memory reuse does not benignly mask the bug
Where this breaks in practice:
  • Use-after-free does not automatically equal RCE; allocator behavior often turns this into a crash-only condition
  • Reliability may vary by build flags, platform, allocator, and surrounding application logic
  • Modern hardening can reduce exploit stability even if DoS remains easy
Detection/coverage: Best coverage is crash telemetry, core dumps, ASan in pre-prod, and EDR/runtime monitoring for service exits or segfaults. Signature-based scanners have weak coverage for this kind of state-machine memory bug.
STEP 04

Impact is service interruption, not broad compromise by default

The published impact is availability loss: remote attackers can crash or destabilize the affected presence-handling service. There is no primary-source claim of in-the-wild exploitation, no KEV entry, and no authoritative RCE claim from upstream. So the likely defender story is telephony or messaging feature disruption in a narrow subset of UC systems, not instant domain-wide compromise.
Conditions required:
  • The target service lacks compensating rate limits or isolation
  • The service restart policy does not fully hide the crash loop
  • The business actually depends on the affected presence/MWI capability
Where this breaks in practice:
  • Blast radius is usually one application/service tier, not arbitrary code execution across the estate
  • HA voice architectures and watchdog restarts can blunt operational impact
  • Many enterprises will have a small absolute count of exposed, affected systems even if they have many total endpoints
Detection/coverage: Monitor SIP daemon crashes/restarts, watchdog loops, sudden drops in presence/MWI signaling, and correlated SUBSCRIBE bursts from a single source.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo primary-source evidence of active exploitation found in this review, and no CISA KEV listing was returned for CVE-2026-28799.
PoC availabilityNo public exploit repo or vendor PoC surfaced in primary-source searching. Reproduction should be straightforward with sipp because the trigger is a scripted SUBSCRIBE / Expires=0 flow, but that's not the same as a packaged public exploit.
EPSSUser-supplied EPSS is 0.00063; Snyk's FIRST-fed display shows 0.06% / 20th percentile, which is consistent with very low current exploitation probability.
KEV statusNot listed in CISA KEV as of this review; no KEV add date because there is no listing.
CVSS vector reality checkAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H correctly captures unauthenticated remote availability impact, but it misses the big real-world gate: the target must be a PJSIP-based presence server/UAS.
Affected versionsUpstream advisory says 2.16 or lower; NVD models it as versions before 2.17.
Fixed versions / distro notesUpstream fix is PJSIP 2.17 via commit e06ff6c. Distro coverage is uneven: Ubuntu shows pjproject as not in release for modern supported releases and vulnerable in legacy 18.04; Debian tracker still showed pjproject and asterisk unfixed at crawl time.
Exposure/scanning realityThere is no dependable Shodan/Censys-style fingerprint for 'PJSIP < 2.17 presence server' because this is a library embedded inside other products. Exposure assessment should come from SBOM/package inventory + SIP service census, not internet-search counts.
Disclosure timelineGHSA published on 2026-03-05; NVD/CVE publication is 2026-03-06; NVD enrichment followed on 2026-03-10.
Researcher / reporterUpstream credits Arthur Chan as finder; the fix notes say it was discovered via AddressSanitizer in CI test suite.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.6/10)

The decisive downgrade factor is reachability: this is not 'all PJSIP everywhere,' it is only the subset of applications acting as a presence server and accepting SUBSCRIBE traffic into the vulnerable callback path. With no KEV, no public exploitation evidence, and availability-only impact in primary sources, the real-world enterprise risk lands in MEDIUM rather than HIGH.

HIGH Technical trigger and fixed version
MEDIUM Population-level exposure reduction from presence/UAS requirement
MEDIUM Assessment that impact is predominantly DoS rather than practical RCE

Why this verdict

  • Down from vendor 7.5 because this is a feature-gated path: the attacker needs a vulnerable app that actually runs the PJSIP presence/MWI/dialog-event UAS logic, not merely any host with the library present.
  • Attacker position is remote, but exposed population is narrow: many enterprises do not expose presence subscription services broadly, and many PJSIP deployments are client-side, internal-only, or wrapped by SBCs/VPNs.
  • No exploitation evidence amplifier: no KEV listing, no public exploit repo found, and EPSS is extremely low, so there is no signal to keep the vendor baseline elevated.
  • Blast radius is mostly service availability: authoritative sources describe availability impact; they do not establish credential theft, data exposure, or reliable code execution.

Why not higher?

A higher rating would require a stronger real-world amplifier: active exploitation, broad external exposure, or a demonstrated path to reliable code execution. We have none of those from primary sources. The vulnerability is real, but it sits behind a specific protocol role and feature path, which compounds the friction materially.

Why not lower?

This is still an unauthenticated network-triggerable memory-safety bug against services that may be business-critical for telephony or messaging workflows. If you do run vulnerable PJSIP-backed presence services, a remote attacker can plausibly cause recurring service disruption without credentials, which is too meaningful to treat as LOW or IGNORE.

05 · Compensating Control

What to do — in priority order.

  1. Restrict SIP reachability — Limit 5060/5061 and related SIP transport exposure to trusted peers, SBCs, VPN ranges, or voice-network segments. If your reassessed severity is MEDIUM, there is no mitigation SLA — go straight to the 365-day remediation window; still, this is the highest-value control when patching has to queue behind more urgent work.
  2. Disable unused presence features — If the product does not need presence, MWI, or dialog-event subscriptions, turn those modules off so the vulnerable path is never reachable. Do this during the next normal change window because feature removal is often cleaner than trying to detect this bug live.
  3. Enforce SIP policy at the edge — Use SBC, SIP proxy, or firewall policy to allow SUBSCRIBE only from known peers and to rate-limit or drop unsolicited subscription traffic. This does not prove non-vulnerability, but it meaningfully reduces unauthenticated internet-origin abuse while you work through inventory.
  4. Monitor for crash loops — Add alerts for SIP daemon restarts, segfaults, watchdog recoveries, and bursts of SUBSCRIBE/Expires=0 requests. For a MEDIUM finding, this is operational hygiene rather than emergency response, but it gives you early warning if the bug becomes noisier than current intel suggests.
What doesn't work
  • A generic WAF does not help much; this is SIP signaling, not normal HTTP application traffic.
  • Relying on EDR alone is weak; EDR may catch a crash after the fact, but it usually will not prevent the protocol-level trigger.
  • Simple port scans are insufficient; finding a SIP listener does not tell you whether the host is running vulnerable PJSIP presence logic.
  • Blocking only REGISTER or INVITE methods misses the issue; the trigger is in SUBSCRIBE with Expires=0.
06 · Verification

Crowdsourced verification payload.

Run this on the target host or in the container/build image that ships the SIP application. Invoke it as python3 verify_pjsip_cve_2026_28799.py with no arguments; it needs only normal read access, though root may help if package databases or system include paths are restricted.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# verify_pjsip_cve_2026_28799.py
# Checks whether a host appears to have PJSIP/pjproject older than 2.17.
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import os
import re
import sys
import glob
import shutil
import subprocess

THRESHOLD = (2, 17)


def run(cmd):
    try:
        p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
        return p.returncode, (p.stdout or '').strip(), (p.stderr or '').strip()
    except Exception:
        return 127, '', 'execution failed'


def parse_version(text):
    if not text:
        return None
    m = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', text)
    if not m:
        return None
    major = int(m.group(1))
    minor = int(m.group(2))
    patch = int(m.group(3) or 0)
    return (major, minor, patch)


def is_patched(ver):
    if ver is None:
        return None
    major, minor, patch = ver
    return (major, minor) >= THRESHOLD


def read_file(path):
    try:
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            return f.read()
    except Exception:
        return ''


def check_pkg_config():
    if not shutil.which('pkg-config'):
        return None, 'pkg-config not found'
    for pkg in ('libpjproject', 'pjproject', 'libpjsip', 'pjsip'):
        rc, out, err = run(['pkg-config', '--modversion', pkg])
        if rc == 0 and out:
            ver = parse_version(out)
            return ver, f'pkg-config:{pkg}={out}'
    return None, 'pkg-config package not found'


def check_pjsua_binary():
    for bin_name in ('pjsua', 'pjsystest'):
        path = shutil.which(bin_name)
        if not path:
            continue
        rc, out, err = run([path, '--version'])
        text = '\n'.join(x for x in (out, err) if x)
        ver = parse_version(text)
        if ver:
            return ver, f'binary:{bin_name}={text}'
    return None, 'no pjsua-style binary version found'


def check_header_files():
    candidates = [
        '/usr/include/pj/version.h',
        '/usr/local/include/pj/version.h',
    ]
    candidates.extend(glob.glob('/opt/**/pj/version.h', recursive=True))
    for path in candidates:
        if not os.path.isfile(path):
            continue
        data = read_file(path)
        maj = re.search(r'#define\s+PJ_VERSION_NUM_MAJOR\s+(\d+)', data)
        mino = re.search(r'#define\s+PJ_VERSION_NUM_MINOR\s+(\d+)', data)
        patch = re.search(r'#define\s+PJ_VERSION_NUM_REV\s+(\d+)', data)
        if maj and mino:
            ver = (int(maj.group(1)), int(mino.group(1)), int(patch.group(1)) if patch else 0)
            return ver, f'header:{path}'
    return None, 'no pj/version.h found'


def check_dpkg():
    if not shutil.which('dpkg-query'):
        return None, 'dpkg-query not found'
    for pkg in ('pjproject', 'libpjproject-dev', 'libpjproject2', 'libpjsip-ua2'):
        rc, out, err = run(['dpkg-query', '-W', '-f=${Version}', pkg])
        if rc == 0 and out and 'no packages found' not in out.lower():
            ver = parse_version(out)
            return ver, f'dpkg:{pkg}={out}'
    return None, 'dpkg package not found'


def check_rpm():
    if not shutil.which('rpm'):
        return None, 'rpm not found'
    for pkg in ('pjproject', 'pjproject-devel', 'pjsip'):
        rc, out, err = run(['rpm', '-q', '--qf', '%{VERSION}-%{RELEASE}', pkg])
        if rc == 0 and out and 'is not installed' not in out.lower():
            ver = parse_version(out)
            return ver, f'rpm:{pkg}={out}'
    return None, 'rpm package not found'


def main():
    checks = [check_pkg_config, check_pjsua_binary, check_header_files, check_dpkg, check_rpm]
    evidence = []
    results = []

    for check in checks:
        ver, note = check()
        evidence.append(note)
        if ver is not None:
            results.append((ver, note, is_patched(ver)))

    # Prefer any direct version evidence we found.
    if results:
        # Sort by version tuple descending just for stable display.
        results.sort(key=lambda x: x[0], reverse=True)
        # If any found version is < 2.17, call it vulnerable unless another check proves the actually loaded/app-bundled copy is newer.
        vulnerable = [r for r in results if r[2] is False]
        patched = [r for r in results if r[2] is True]

        if vulnerable:
            ver, note, _ = vulnerable[0]
            print(f'VULNERABLE - found PJSIP/pjproject version {ver[0]}.{ver[1]}.{ver[2]} via {note}')
            sys.exit(1)
        if patched:
            ver, note, _ = patched[0]
            print(f'PATCHED - found PJSIP/pjproject version {ver[0]}.{ver[1]}.{ver[2]} via {note}')
            sys.exit(0)

    print('UNKNOWN - could not identify an installed PJSIP/pjproject version; inspect application-bundled libraries, SBOM, or build manifests')
    sys.exit(2)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: first identify whether you actually run PJSIP-backed presence/MWI/dialog-event servers anywhere internet-reachable or partner-reachable; if not, this drops into normal maintenance. For this MEDIUM reassessment there is no noisgate mitigation SLA — go straight to the 365-day remediation window; apply the upstream 2.17 fix or an equivalent vendor backport within the noisgate remediation SLA of 365 days, and tighten SIP exposure opportunistically where these services are externally reachable.

Sources

  1. NVD CVE-2026-28799
  2. GitHub Security Advisory GHSA-8fj4-fv9f-hjpc
  3. Upstream fix commit e06ff6c
  4. PJSIP 2.17 release
  5. PJSIP project overview/homepage
  6. Snyk record with EPSS display
  7. Red Hat Bugzilla tracking
  8. Debian security tracker
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.