OpenSIPS SQL Injection to Full Authentication Bypass (CVE-2026-25554)
Author
Joshua RogersDate Published

The AISLE analyzer discovered a high-severity vulnerability in OpenSIPS, one of the most widely deployed open-source components in telecom and VoIP infrastructure. OpenSIPS helps providers route calls, register users, and enforce authentication across large-scale voice networks, which means weaknesses in this layer can have direct consequences for service security and trust.
The issue, assigned CVE-2026-25554 with a severity score of 8.3, allowed SQL injection in a code path used for authentication. In practice, this meant a remote attacker could exploit the flaw to impersonate arbitrary SIP users without needing prior access to the environment. In affected deployments, that allowed an attacker to register as another subscriber, place calls under a trusted identity, or access services protected by SIP authentication.
The vulnerability affects all OpenSIPS versions 3.1 and later that ship with the auth_jwt module, prior to the patch accepted upstream in PR #3807 in February. In the sections that follow, we explain how the issue works, and how it was fixed following responsible disclosure with the OpenSIPS maintainers.
OpenSIPS: The Basics
OpenSIPS is an open-source SIP (Session Initiation Protocol) server used heavily in VoIP and real-time communication infrastructure. If you've made a VoIP call, there's a reasonable chance an OpenSIPS instance was involved.
When you dial someone via VoIP, SIP locates the other party and negotiates the session. It also handles the entire state machine of a call, ranging from ringing to holds to hanging up. (Note that SIP only handles signaling, and voice data travels over RTP (Real-time Transport Protocol), which is a separate path.)
As an open source SIP proxy/server, OpenSIPS enables enterprise-grade throughput with millions of simultaneous calls. For that reason, it is widely used by carriers and providers around the world.
How SIP authentication works
When a SIP client such as a phone, softphone, or gateway places a call, it sends a SIP request (such as REGISTER or INVITE) to a SIP proxy (like OpenSIPS). The proxy needs to verify the client's identity before it routes anything.
Traditionally, SIP clients authenticate via HTTP Digest, in which the server presents clients with a nonce and clients respond by deriving a hash from their user credentials. However, this approach doesn’t compose well with modern identity systems for a number of reasons (it can’t delegate to external identity providers, and it doesn’t support token-based SSO).
OpenSIPS’s auth_jwt module avoids this issue with JWT-based authentication, in which clients construct and sign a JWT (JSON Web Token) using a pre-shared HMAC secret.The JWT includes a tag claim that specifies which cryptographic key OpenSIPS must use during signature validation (it essentially acts as a key selector).
Once it receives the token, the server:
- Reads the
tagclaim to determine which key profile to look up; - Queries the database for the corresponding signing secret;
- Verifies the JWT signature against the secret.
The vulnerability breaks step 2 of this process.
The Vulnerability
The vulnerable function, jwt_db_authorize(), lives in modules/auth_jwt/authorize.c. The function starts by decoding the JWT to obtain the tag claim:
C1if (jwt_decode(&jwt, jwt_token_buf, NULL, 0) != 0 || jwt == NULL) {2LM_ERR("Failed to decode jwt \n");3goto err_out;4}56tag_s = (char *)jwt_get_grant(jwt, (const char *)jwt_tag_claim.s);
The NULL, 0 parameters mean "decode but don't verify" (in libjwt, passing NULL/0 for the key parameters skips signature verification), so at this point, tag_s contains whatever string the sender put in the tag claim. That is, it’s raw, attacker-controlled input.
Next, tag_s is inserted directly into a raw SQL query using snprintf:
C1tag.s = tag_s;2tag.len = strlen(tag_s);34/* ... */56n = snprintf(p, len,7" from %.*s a inner join %.*s b on a.%.*s = b.%.*s"8" where a.%.*s='%.*s'"9" and %ld >= b.%.*s and %ld < b.%.s",10profiles_table.len, profiles_table.s,11secrets_table.len, secrets_table.s,12tag_column.len, tag_column.s,13secret_tag_column.len, secret_tag_column.s,14tag_column.len, tag_column.s,15tag.len, tag.s, / <-- attacker-controlled, unescaped */16unix_ts, start_ts_column.len, start_ts_column.s,17unix_ts, end_ts_column.len, end_ts_column.s);
Note that the tag value is placed directly inside the single-quoted SQL string '%.*s' with no escaping. In other words, this is a textbook case of SQL injection.
Attack Overview
Here’s what the query this function builds looks like:
JavaScript1SELECT a.sip_username, b.secret2FROM jwt_profiles a3INNER JOIN jwt_secrets b ON a.tag = b.corresponding_tag4WHERE a.tag='<ATTACKER_INPUT>'5AND <unix_ts> >= b.start_ts6AND <unix_ts> < b.end_ts
To exploit, all an attacker needs to do is craft a JWT with a tag claim designed to break out of the quoted string and inject a UNION SELECT. For instance:
JavaScript1' UNION SELECT 'admin','attacker_secret' --
Note that since the injected UNION SELECT has to satisfy the column count and types, attackers must pad the injected row with additional expressions to match the number of columns in the original SELECT.
After interpolation, you get:
JavaScript1SELECT a.sip_username, b.secret2FROM jwt_profiles a3INNER JOIN jwt_secrets b ON a.tag = b.corresponding_tag4WHERE a.tag='' UNION SELECT 'admin','attacker_secret' --'5AND <unix_ts> >= b.start_ts6AND <unix_ts> < b.end_ts
The -- comments out the rest of the query, which lets the UNION SELECT inject append a fake row such as:
sip_username=adminsecret=attacker_secret
Thus, when the function iterates over query results and attempts to verify the JWT signature against each returned secret, it does so using attacker-provided credentials:
C1for (i = 0; i < RES_ROW_N(res); i++) {2 row = RES_ROWS(res) + i;3 secret.s = (char *)VAL_STRING(ROW_VALUES(row) + 1);4 secret.len = strlen(secret.s);56 if (jwt_decode(&jwt_dec, jwt_token_buf,7 (const unsigned char *)secret.s, secret.len) != 0 ||8 jwt_dec == NULL) {9 continue; /* signature didn't match, try next */10 }1112 /* Signature verified -- user is authenticated */13 return 1;14}
The attacker signs their JWT with the HMAC secret attacker_secret. The server then retrieves attacker_secret from the injected query result, verifies the signature against it, and lo and behold, it matches.
Impact
The result is a complete authentication bypass. Because the attacker controls both the injected secret and the sip_username value in the UNION SELECT, they can impersonate any user. In a typical deployment, this means an attacker can register and place calls from any SIP address or access any resource guarded by JWT auth, opening the door to social engineering attacks, toll fraud, wiretapping, and other mischief.
In other words:
All OpenSIPS deployments using auth_jwt with database mode enabled (db_url configured) are affected, but note that deployments using only jwt_script_authorize() with script-provided keys are not part of this code path.
The Fix
After identifying the vulnerability in January 2026, the AISLE analyzer proposed a simple fix, which was accepted by the OpenSIPS maintainers and implemented on February 2, 2026. We followed responsible disclosure practices and worked closely with the OpenSIPS maintainers throughout the process.
In essence, the fix escapes the tag claim before interpolating it into the SQL query. It does so by adding a call to escape_common(), which escapes single quotes, double quotes, backslashes, and null bytes:
C1escaped_tag_buf = pkg_malloc(tag.len * 2 + 1);2if (!escaped_tag_buf) {3 LM_ERR("No more pkg mem for escaped tag\n");4 goto err_out;5}67escaped_tag.len = escape_common(escaped_tag_buf, tag.s, tag.len);8escaped_tag.s = escaped_tag_buf;
The query then uses escaped_tag instead of the raw tag:
C1n = snprintf(p, len,2 " from %.*s a inner join %.*s b on a.%.*s = b.%.*s"3 " where a.%.*s='%.*s'"4 " and %ld >= b.%.*s and %ld < b.%.*s",5 /* ... */6 escaped_tag.len, escaped_tag.s, /* escaped, not raw */7 /* ... */);
Now, the attacker-controlled tag value is escaped before it is placed into the SQL string literal, which stops it from terminating the quoted value and injecting SQL. Note that because the auth_jwt query requires a JOIN across two tables, it cannot use OpenSIPS's structured DB API, which only operates on single tables. This makes manual escaping via escape_common() the appropriate defense for the raw query path.
Talk to Us
We found this vulnerability by using our autonomous analyzer to review the OpenSIPS codebase. If you’re interested in learning more about our research, contact us at [email protected].
Our appreciation goes to the OpenSIPS team for their collaboration throughout this process. This vulnerability was discovered by Pavel Kohout using AISLE’s autonomous analyzer.