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.
4 steps from start to impact.
Find a reachable PJSIP-backed SIP service
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.- 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
- 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
Reach the presence subscription server path
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.- Application supports presence, MWI, or dialog-event subscriptions
- The vulnerable service accepts attacker-controlled
SUBSCRIBEtraffic - The specific UAS callback flow is enabled in the product build/configuration
- 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
SUBSCRIBE with Expires=0, but generic vuln scanners usually miss this feature-specific reachability question.Trigger the callback-ordering use-after-free
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.- 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
- 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
Impact is service interruption, not broad compromise by default
- 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
- 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
SUBSCRIBE bursts from a single source.The supporting signals.
| In-the-wild status | No primary-source evidence of active exploitation found in this review, and no CISA KEV listing was returned for CVE-2026-28799. |
|---|---|
| PoC availability | No 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. |
| EPSS | User-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 status | Not listed in CISA KEV as of this review; no KEV add date because there is no listing. |
| CVSS vector reality check | AV: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 versions | Upstream advisory says 2.16 or lower; NVD models it as versions before 2.17. |
| Fixed versions / distro notes | Upstream 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 reality | There 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 timeline | GHSA published on 2026-03-05; NVD/CVE publication is 2026-03-06; NVD enrichment followed on 2026-03-10. |
| Researcher / reporter | Upstream credits Arthur Chan as finder; the fix notes say it was discovered via AddressSanitizer in CI test suite. |
noisgate verdict.
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.
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.
What to do — in priority order.
- Restrict SIP reachability — Limit
5060/5061and 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. - 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.
- Enforce SIP policy at the edge — Use SBC, SIP proxy, or firewall policy to allow
SUBSCRIBEonly 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. - Monitor for crash loops — Add alerts for SIP daemon restarts, segfaults, watchdog recoveries, and bursts of
SUBSCRIBE/Expires=0requests. 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.
- 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
SUBSCRIBEwithExpires=0.
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.
#!/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()
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.