TL;DR
-
What: In GOautodial's
goAPIv2agent API, thegoPhonerequest parameter is escaped for SQL (mysqli_real_escape_string) and then concatenated into anexec()command string. Shell metacharacters survive the SQL escape, so a$(...)payload runs on the host. - Impact: Any authenticated low-privilege agent — a role with no legitimate shell access — gets arbitrary OS command execution on the server running the API. CWE-78, CVSS 3.1 8.8 (High).
-
Fixed in: the installer-shipped
goAPIv2master branch (commit0ab2584). Reported by me, Santosh Kumar Puppala — CVE requested/pending.
Why you should care
GOautodial is a widely deployed open-source call-center / predictive-dialer platform built on Asterisk. Its goAPIv2 component is the REST API that agent softphones talk to — it ships to /var/www/html via the installer and is reachable by anyone with an agent login.
Agents are the low rung of the trust ladder. They log in, take calls, and hang up. They are absolutely not supposed to be able to run shell commands on the dialer host. This bug hands exactly that capability to any agent account, and on a real deployment the injected command runs through a sudo wrapper — so the practical blast radius is worse than the already-High score implies.
The reason this class of bug keeps shipping is that it hides behind a function whose name sounds like sanitization. It escapes something. It just escapes it for the wrong language.
The setup
The endpoint is the agent "log out" action:
GET /goAgent/goAPI.php?goAction=goLogoutUser&goPhone=8300&goSIPServer=asterisk&goUseWebRTC=0
Reaching it needs only an ordinary active agent (vicidial_users.user_level = 1, active = 'Y', vdc_agent_api_access = 1). goLogoutUser is on the API's no-campaign-required allowlist, so it's available to essentially every agent. As part of logging an agent out, the code checks the SIP registration for the agent's phone/extension — and that check is where goPhone ends up.
The bug
Here is the whole chain, four short hops from request parameter to shell.
1. The value is escaped — for SQL.
// goAgent/goAPI.php
$phone_login = $astDB->escape($_GET['goPhone']);
// goAgent/includes/MySQLiDB.php:931
return $this->_mysqli->real_escape_string($str);
mysqli_real_escape_string exists to neutralize SQL quoting — quotes, backslashes, NUL. It does nothing to shell metacharacters. Backtick, $, (, ), |, &, ; all pass through untouched. As a shell defense, this call is decorative.
2. The SQL-escaped value flows into a SIP check.
// goAgent/goLogoutUser.php:54
check_sip_login($kamDB, $phone_login, $SIPserver, $use_webrtc);
check_sip_login() is a SIP-registration lookup, not an operator command runner. Nobody reading this call site expects it to be a command sink — which is precisely why the missing validation went unnoticed.
3. It's concatenated into an exec() string.
// goFunctions.php:460
exec('/usr/share/goautodial/goautodialc.pl "sudo /usr/sbin/asterisk -rx \"sip show peer '.$exten.'\""', $output);
$exten is the request's goPhone. PHP's exec() runs its argument through /bin/sh -c, so any command substitution inside $exten is expanded by the shell, at the PHP layer, before the Perl helper is ever invoked.
The "aha":
The value had been escaped — the developer just escaped it for the wrong language. A SQL escape stops a SQL injection; it does not stop a shell from reading
$(...).
Proof of concept
I reproduced this live end-to-end against the unmodified goAPIv2 source (HEAD 640a31f) in a local PHP 8.2 + Apache + MariaDB stack, authenticated as a low-privilege agent. Everything here is a benign marker — no destructive payload, just proof that the shell ran my command.
Control — a benign extension behaves normally:
?...&goAction=goLogoutUser&goPhone=8300&goSIPServer=asterisk&goUseWebRTC=0
→ wrapper called with: sip show peer 8300
→ /tmp/goauto_pwn : absent (correct — nothing extra ran)
Injection — a command substitution in goPhone:
goPhone=8300$(touch /tmp/goauto_pwn)
→ /tmp/goauto_pwn : -rw-r--r-- 1 www-data www-data 0 ... /tmp/goauto_pwn (created)
goPhone=8300$(id>/tmp/goauto_out)
→ /tmp/goauto_out : uid=33(www-data) gid=33(www-data) groups=33(www-data)
The injected commands ran as www-data, the Apache/PHP user. The wrapper's own call log still showed only sip show peer 8300, which confirms the substitution expanded at PHP's exec()//bin/sh -c layer — independent of anything the Perl helper does. On a production host that helper runs under sudo, so effective privilege climbs from there.
The fix
Never build a shell string from request-derived input. Pass the extension as a separate, validated argument, and constrain it to the format it's actually allowed to be:
// escape for the shell, not for SQL — and validate the format first
if (!preg_match('/^\d{1,10}$/', $exten)) { /* reject */ }
$cmd = '/usr/share/goautodial/goautodialc.pl ' . escapeshellarg(
'sudo /usr/sbin/asterisk -rx "sip show peer ' . $exten . '"'
);
exec($cmd, $output);
Better still, use an exec form that takes an argv array so no shell is involved at all. The upstream fix landed on the installer-shipped goAPIv2 master branch (commit 0ab2584).
Takeaways
-
"Escaped" is not a property of a string — it's a property of a string for a specific sink. A value that's safe for a SQL driver is not thereby safe for a shell, an HTML page, a
Runtime.exec, or an LDAP filter. Track which escape a value carries, and re-escape at every new sink. -
Audit by data flow, not by function name.
check_sip_login()sounds harmless. The vulnerability was three hops downstream in a helper nobody associates with user input. Grep forexec/shell_exec/system/passthruand walk backwards to the request. - Low-privilege reachability is what turns a code smell into an 8.8. The same sink behind an admin-only screen is a minor hardening note. Behind an ordinary agent login, it's remote code execution for your least-trusted authenticated role.
Disclosure timeline
-
2026-07-01 — Found during a source audit of
goautodial/goAPIv2; source→sink traced. - 2026-07-06 — Reproduced live end-to-end in a local Docker stack (benign markers), verdict CONFIRMED.
- Reported to the maintainers via coordinated disclosure; credit agreed.
-
Fixed on the installer-shipped
goAPIv2master branch (commit0ab2584). CVE requested/pending.
Credit
Reported and written by Santosh Kumar Puppala — AI/ML Platform Architect and security researcher (multiple CVEs; creator of Norviq & Veridor). GitHub: @Santoshkumarpuppala.
If you run GOautodial, update to a build that includes commit 0ab2584 and audit your other exec() call sites for request-derived arguments while you're in there. Found this useful? Follow along — I publish one of these walkthroughs every few days.

Top comments (0)