diff --git a/src/Public/Controllers/Admin/TableController.php b/src/Public/Controllers/Admin/TableController.php index b0394f09..9d85c911 100644 --- a/src/Public/Controllers/Admin/TableController.php +++ b/src/Public/Controllers/Admin/TableController.php @@ -4892,26 +4892,30 @@ class TableController extends BaseAdminController { if ($rIsAPI) { $rReturn["data"][] = self::filterRow($rRow, RequestManager::get("show_columns") ?? '', RequestManager::get("hide_columns") ?? ''); } else { + // Every value below except the admin's own provider row comes + // from the remote provider's API (ProvidersCronJob), and the + // table renders these cells as HTML: escape each one for where + // it lands — an attribute, a JS string inside an attribute, text. if ($rRow["type"] == "live") { $rStreamURL = ($rRow["ssl"] ? "https" : "http") . "://" . $rRow["ip"] . ":" . $rRow["port"] . "/live/" . $rRow["username"] . "/" . $rRow["password"] . "/" . $rRow["stream_id"] . ($rRow["hls"] ? ".m3u8" : ($rRow["legacy"] ? ".ts" : "")); - $rButtons = ""; + $rButtons = ""; } else { $rStreamURL = ($rRow["ssl"] ? "https" : "http") . "://" . $rRow["ip"] . ":" . $rRow["port"] . "/movie/" . $rRow["username"] . "/" . $rRow["password"] . "/" . $rRow["stream_id"] . "." . $rRow["channel_id"]; - $rButtons = ""; + $rButtons = ""; } if ((string) $rRow["stream_icon"] !== '' && $rRow["type"] == "live") { - $rIcon = ""; + $rIcon = ""; } else { $rIcon = ""; } - $rProviderData = json_decode($rRow["data"], true); - $rExpires = $rProviderData["exp_date"] ?: "Never"; - $rMaxConnections = $rProviderData["max_connections"] ?: "∞"; - $rProvider = "" . $rRow["name"] . ""; + $rProviderData = json_decode((string) $rRow["data"], true) ?: []; + $rExpires = ($rProviderData["exp_date"] ?? null) ? self::htmlValue($rProviderData["exp_date"]) : "Never"; + $rMaxConnections = ($rProviderData["max_connections"] ?? null) ? self::htmlValue($rProviderData["max_connections"]) : "∞"; + $rProvider = "" . self::htmlValue($rRow["name"]) . ""; if ($rRow["type"] == "live") { - $rReturn["data"][] = [$rIcon, $rRow["stream_display_name"], $rProvider, $rButtons]; + $rReturn["data"][] = [$rIcon, self::htmlValue($rRow["stream_display_name"]), $rProvider, $rButtons]; } else { - $rReturn["data"][] = [$rRow["stream_display_name"], $rProvider, $rButtons]; + $rReturn["data"][] = [self::htmlValue($rRow["stream_display_name"]), $rProvider, $rButtons]; } } } @@ -4921,6 +4925,20 @@ class TableController extends BaseAdminController { exit; } + /** + * A database value made safe for HTML text or a quoted attribute. Rows arrive + * with only < and > entity-encoded (Database::clean_row), so they are decoded + * first and encoded once, quotes included. + */ + private static function htmlValue(mixed $rValue): string { + return htmlspecialchars(html_entity_decode((string) $rValue, ENT_QUOTES), ENT_QUOTES); + } + + /** A database value as a JavaScript string literal, for an event-handler attribute. */ + private static function jsArgument(mixed $rValue): string { + return htmlspecialchars((string) json_encode(html_entity_decode((string) $rValue, ENT_QUOTES), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), ENT_QUOTES); + } + private function handleParentServers($rReturn, $rStart, $rLimit, $rIsAPI) { global $db, $rServers; if (!Authorization::check("adv", "servers")) {