Atomic Edge analysis of CVE-2026-5247:
This vulnerability is a Stored Cross-Site Scripting (XSS) in the Schedule Post Changes With PublishPress Future plugin for WordPress, affecting versions up to and including 4.10.0. The issue resides in the [futureaction] shortcode’s ‘wrapper’ attribute. An authenticated attacker with administrator-level access (or lower if admins have made the shortcode available) can inject arbitrary HTML event handlers or script tags, leading to persistent XSS. The CVSS score is 5.5, indicating medium severity.
Root Cause: The vulnerability stems from insufficient sanitization of the ‘wrapper’ attribute in the shortcode rendering code. The plugin uses esc_html() to escape the attribute value. However, esc_html() only encodes HTML entities (like &) and does not prevent attribute injection when the value is used as an HTML tag name in a sprintf() call. For example, the code might produce where [wrapper] is a user-controlled string. An attacker can include spaces in the wrapper value to break out of the tag name context and inject event handler attributes such as onmouseover, onclick, or onerror. The exact vulnerable code is not shown in the truncated diff, but the shortcode handler (likely in a file like post-expirator/…/shortcode.php or similar) uses sprintf() with the wrapper parameter directly.
Exploitation: An attacker with administrator-level access inserts a [futureaction] shortcode into a post or page. They set the wrapper attribute to a value containing spaces and event handler attributes, e.g., wrapper=”div onmouseover=alert(1) “. The resulting HTML becomes
Patch Analysis: The provided diff is a language file update for German (de_DE) and does not show the actual code fix for the wrapper attribute. The true patch, which would appear in a separate diff, likely replaces esc_html() with a more restrictive sanitization function like sanitize_html_class() or a custom whitelist that only allows alphanumeric characters and hyphens/underscores, preventing spaces and attribute injection. Alternatively, the patch might escape the wrapper value for use as a tag name using functions that strip spaces and special characters. The language file diff is irrelevant to the security fix.
Impact: Successful exploitation allows an attacker to inject arbitrary JavaScript into WordPress pages. This can lead to session hijacking, credential theft, redirecting users to malicious sites, defacement, or performing actions on behalf of any logged-in user who views the page. Because the XSS is stored, any visitor to the compromised page becomes a target, including other administrators, potentially leading to full site compromise.
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/post-expirator/languages/post-expirator-de_DE.l10n.php
+++ b/post-expirator/languages/post-expirator-de_DE.l10n.php
@@ -1,2 +1,6 @@
<?php
-return ['domain'=>'post-expirator','plural-forms'=>'nplurals=2; plural=n != 1;','language'=>'de-DE','project-id-version'=>'PublishPress Future 4.10.0','pot-creation-date'=>'2026-03-10T20:37:16+00:00','po-revision-date'=>'2026-03-24 19:13+0000','x-generator'=>'WP-CLI 2.12.0','messages'=>['PublishPress Future Free'=>'PublishPress Future Free','http://wordpress.org/extend/plugins/post-expirator/'=>'http://wordpress.org/extend/plugins/post-expirator/','PublishPress'=>'PublishPress','http://publishpress.com'=>'http://publishpress.com','You do not have permission to configure PublishPress Future.'=>'Sie haben keine Berechtigung, PublishPress Future zu konfigurieren.','Debug is disabled'=>'Debugging ist deaktiviert','Debugging Disabled'=>'Debugging Deaktiviert','Debugging Enabled'=>'Debugging Aktiviert','Debugging Table Emptied'=>'Debugging-Tabelle geleert','The database schema was fixed.'=>'Das Datenbankschema wurde repariert.','The database schema could not be fixed. Please, contact the support team.'=>'Das Datenbankschema konnte nicht repariert werden. Bitte kontaktieren Sie das Support-Team.','Saved Options!'=>'Gespeicherte Optionen!','If you like %1$s, please leave us a %2$s rating. Thank you!'=>'Wenn Sie %1$s mögen, hinterlassen Sie uns bitte eine %2$s Bewertung. Danke!','About PublishPress Future'=>'Über PublishPress Future','About'=>'Über','Future Documentation'=>'Zukünftige Dokumentation','Documentation'=>'Dokumentation','Contact the PublishPress team'=>'Kontaktieren Sie das PublishPress-Team','Contact'=>'Kontakt','No taxonomies found'=>'Keine Taxonomien gefunden','l F jS, Y'=>'l. F jS, Y','g:ia'=>'g:ia','Post expires at EXPIRATIONTIME on ACTIONDATE'=>'Der Beitrag läuft am EXPIRATIONTIME am ACTIONDATE ab','The table %s does not exist.'=>'Die Tabelle %s existiert nicht.','The table indexes are invalid: '=>'Die Tabellenindizes sind ungültig: ','Empty date time offset.'=>'Leerer Datums-Zeit-Versatz.','Invalid date time offset'=>'Ungültiger Datums-Zeit-Versatz','Export'=>'Exportieren','Import'=>'Importieren','Failed to get workflows. Check the logs for more details.'=>'Fehler beim Abrufen der Workflows. Überprüfen Sie die Protokolle für weitere Details.','Failed to export the file. Check the logs for more details.'=>'Fehler beim Exportieren der Datei. Überprüfen Sie die Protokolle für weitere Details.','Failed to import the file. Check the logs for more details.'=>'Fehler beim Importieren der Datei. Überprüfen Sie die Protokolle für weitere Details.','You do not have sufficient permissions to access this page.'=>'Sie haben nicht genügend Berechtigungen, um auf diese Seite zuzugreifen.','Invalid nonce.'=>'Ungültiger Nonce.','Add millisecond precision to debug log timestamp column (v4.10.0)'=>'Fügen Sie der Debug-Protokoll-Zeitstempelspalte Millisekundenpräzision hinzu (v4.10.0)','Debugging table is currently empty.'=>'Die Debugging-Tabelle ist derzeit leer.','No results match the current filter.'=>'Keine Ergebnisse entsprechen dem aktuellen Filter.','Taxonomy'=>'Taxonomie','Future Actions'=>'Zukünftige Aktionen','Enable Future Action'=>'Zukünftige Aktion aktivieren','Categories'=>'Kategorien','Action'=>'Aktion','Loading'=>'Wird geladen','Show Calendar'=>'Kalender anzeigen','Hide Calendar'=>'Kalender ausblenden','Timezone is controlled by the {WordPress Settings}.'=>'Die Zeitzone wird von den {WordPress-Einstellungen} gesteuert.','No %s found.'=>'Keine %s gefunden.','You must assign a taxonomy to this post type to use this feature.'=>'Sie müssen eine Taxonomie diesem Beitragstyp zuweisen, um diese Funktion zu nutzen.','New %s'=>'Neue %s','%s to remove'=>'%s zum Entfernen','%s to add'=>'%s zum Hinzufügen','Search for %s'=>'Nach %s suchen','Select an action'=>'Wählen Sie eine Aktion aus','Select a date'=>'Wählen Sie ein Datum','Date cannot be in the past'=>'Das Datum darf nicht in der Vergangenheit liegen','Please select one or more %s'=>'Bitte wählen Sie eine oder mehrere %s aus','New status'=>'Neuer Status','No posts selected. Unable to sync Future Actions.'=>'Keine Beiträge ausgewählt. Zukünftige Aktionen können nicht synchronisiert werden.','Future Actions successfully synced with Post Metadata.'=>'Zukünftige Aktionen erfolgreich mit Beitragsmetadaten synchronisiert.','Update Future Actions from Post Metadata'=>'Zukünftige Aktionen aus Beitragsmetadaten aktualisieren','Future Action Update'=>'Aktualisierung zukünftiger Aktionen','— No Change —'=>'— Keine Änderung —','Add or update action for posts'=>'Aktion für Beiträge hinzufügen oder aktualisieren','Add action if none exists for posts'=>'Aktion hinzufügen, wenn keine für Beiträge vorhanden ist','Update the existing actions for posts'=>'Die vorhandenen Aktionen für Beiträge aktualisieren','Remove action from posts'=>'Aktion von Beiträgen entfernen','Category'=>'Kategorie','Upgrade to Pro'=>'Upgrade auf Pro','Action Workflows'=>'Aktions-Workflows','Settings'=>'Einstellungen','Future Action'=>'Zukünftige Aktion','No future action'=>'Keine zukünftige Aktion','Invalid date time offset.'=>'Ungültiger Datums- und Uhrzeitversatz.','PublishPress Future'=>'PublishPress Future','Future'=>'Zukunft','Scheduled Actions'=>'Geplante Aktionen','Log format'=>'Protokollformat','List'=>'Liste','Popup'=>'Popup','Unknown post'=>'Unbekannter Beitrag','Executed action for: %s (ID: %d)'=>'Ausgeführte Aktion für: %s (ID: %d)','Executed scheduled action'=>'Geplante Aktion ausgeführt','Default Values'=>'Standardwerte','Active'=>'Aktiv','Activate the PublishPress Future actions for this post type'=>'Aktivieren Sie die PublishPress Future-Aktionen für diesen Beitragstyp','Default Action'=>'Standardaktion','Select the default action for the post type.'=>'Wählen Sie die Standardaktion für den Beitragstyp aus.','Select the taxonomy to be used for actions.'=>'Wählen Sie die Taxonomie aus, die für die Aktionen verwendet werden soll.','Automatically create actions'=>'Aktionen automatisch erstellen','Who to Notify'=>'Wer benachrichtigt werden soll','Default Date/Time Offset'=>'Standard-Datum/Uhrzeit-Offset','PHP strtotime function'=>'PHP strtotime-Funktion','Default terms:'=>'Standardbedingungen:','Save changes'=>'Änderungen speichern','Wait for the validation...'=>'Warten auf die Validierung...','Date Preview'=>'Datumsvorschau','Current Date'=>'Aktuelles Datum','Computed Date'=>'Berechnetes Datum','Error'=>'Fehler','Custom statuses'=>'Benutzerdefinierte Status','Enable support for custom statuses'=>'Unterstützung für benutzerdefinierte Status aktivieren','Checking this option will allow you to move posts to a custom status.'=>'Durch Aktivieren dieser Option können Sie Beiträge in einen benutzerdefinierten Status verschieben.','Metadata scheduling'=>'Metadata-Planung','Enable metadata scheduling'=>'Metadata-Planung aktivieren','This is a Pro feature. Upgrade to unlock this functionality.'=>'Dies ist eine Pro-Funktion. Upgrade, um diese Funktionalität freizuschalten.','Form Validation Failure: Sorry, your nonce did not verify.'=>'Formularvalidierungsfehler: Entschuldigung, Ihr Nonce konnte nicht verifiziert werden.','The column args length was not updated to 1000.'=>'Die Länge der Spaltenargumente wurde nicht auf 1000 aktualisiert.','Status didn't change.'=>'Status hat sich nicht geändert.','Status has been successfully changed from "%1$s" to "%2$s".'=>'Der Status wurde erfolgreich von "%1$s" auf "%2$s" geändert.','Change status'=>'Status ändern','%s was not deleted.'=>'%s wurde nicht gelöscht.','%s has been successfully deleted.'=>'%s wurde erfolgreich gelöscht.','Delete'=>'Löschen','No terms were added to the %s.'=>'Es wurden keine Begriffe zum %s hinzugefügt.','Add extra %s'=>'Zusätzlich %s hinzufügen','No terms were removed from the %s.'=>'Es wurden keine Begriffe vom %s entfernt.','Remove selected %s'=>'Ausgewählte %s entfernen','The following terms (%1$s) were removed from the %2$s: %3$s.'=>'Die folgenden Begriffe (%1$s) wurden vom %2$s entfernt: %3$s.','Remove all %s'=>'Alle %s entfernen','No terms were changed on the %s.'=>'Es wurden keine Begriffe im %s geändert.','Replace all %s'=>'Alle %s ersetzen','Change status to %s'=>'Status auf %s ändern','%s didn't change.'=>'%s hat sich nicht geändert.','%s has been added to stickies list.'=>'%s wurde zur Liste der Haftnotizen hinzugefügt.','Stick'=>'Festhalten','%s has been removed from stickies list.'=>'%s wurde von der Liste der Haftnotizen entfernt.','Unstick'=>'Loslassen','Migrate legacy actions arguments schema after v3.0.0'=>'Schema der Argumente für veraltete Aktionen nach v3.0.0 migrieren','Migrate legacy footer placeholders after v3.0.0'=>'Platzhalter für veraltete Fußzeilen nach v3.0.0 migrieren','Migrate legacy scheduled actions after v3.0.0'=>'Geplante veraltete Aktionen nach v3.0.0 migrieren','Restore post meta data after v3.0.1'=>'Beitragsmetadaten nach v3.0.1 wiederherstellen','Email is disabled.'=>'E-Mail ist deaktiviert.','Email sent.'=>'E-Mail gesendet.','Email not sent.'=>'E-Mail nicht gesendet.','%1$s. %2$s on %3$s. The post link is %4$s'=>'%1$s. %2$s am %3$s. Der Beitraglink ist %4$s','Future Action Complete "%s"'=>'Zukünftige Aktion abgeschlossen "%s"','[%1$s] %2$s'=>'[%1$s] %2$s','Status'=>'Status','Arguments'=>'Argumente','Logs'=>'Protokolle','Scheduled Date'=>'Geplantes Datum','Recurrence'=>'Wiederkehr','Run now'=>'Jetzt ausführen','Cancel'=>'Abbrechen','Cancel the action. This will prevent the action from running in the future'=>'Die Aktion abbrechen. Dies verhindert, dass die Aktion in Zukunft ausgeführt wird','Run'=>'Ausführen','%s year'=>'%s Jahr' . "
