From b982cb40be489500ecf6f4533bc697a3540a05f6 Mon Sep 17 00:00:00 2001 From: Hasan Khan Date: Sat, 30 May 2026 20:43:48 -0500 Subject: [PATCH 1/2] Fix multiple security issues in legacy endpoints - search_redirect.php: replace string-concatenated INSERT (with attacker-controlled X-Forwarded-For / HTTP_CLIENT_IP flowing in unescaped) with a parameterized prepared statement; stop echoing mysqli errors to the response. - share.php: HTML-escape $_POST['link'] before interpolating into href attributes and restrict it to a site-relative path; prevents reflected XSS via crafted POST. - IndexController::actionFlushCache: require POST and restrict to loopback callers so anonymous users can no longer flush the cache (cache-stampede DoS) or delete arbitrary cache keys. - ElasticConnection: strip CR/LF and other control bytes from User-Agent and the forwarded client IP before placing them in outbound HTTP headers; prevents header / request smuggling into the Elasticsearch backend. - processer.php: validate the user-supplied Reply-To address with FILTER_VALIDATE_EMAIL and reject CR/LF; prevents mail-header injection / open-relay abuse of the SES credentials. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../search/engines/ElasticConnection.php | 19 +++++++++++++++-- .../front/controllers/IndexController.php | 13 ++++++++++-- public/processer.php | 10 +++++++-- public/search_redirect.php | 18 +++++++++++----- public/share.php | 21 ++++++++++++++----- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/application/components/search/engines/ElasticConnection.php b/application/components/search/engines/ElasticConnection.php index 8f779777..0cca6d67 100644 --- a/application/components/search/engines/ElasticConnection.php +++ b/application/components/search/engines/ElasticConnection.php @@ -36,13 +36,13 @@ private function getHeaders() 'Authorization: Basic ' . base64_encode($this->username.':'.$this->password), ); - $clientIp = $this->getClientIp(); + $clientIp = $this->sanitizeHeaderValue($this->getClientIp()); if (!empty($clientIp)) { $headers[] = 'X-Forwarded-For: ' . $clientIp; $headers[] = 'X-Real-IP: ' . $clientIp; } - $userAgent = Yii::$app->getRequest()->getUserAgent(); + $userAgent = $this->sanitizeHeaderValue(Yii::$app->getRequest()->getUserAgent()); if (!empty($userAgent)) { $headers[] = 'User-Agent: ' . $userAgent; } @@ -50,6 +50,21 @@ private function getHeaders() return $headers; } + /** + * Strip CR/LF and other control characters from a value before placing it + * in an outbound HTTP header. Prevents header / request smuggling via + * client-controlled headers like User-Agent and X-Forwarded-For. + */ + private function sanitizeHeaderValue($value) + { + if ($value === null) { + return ''; + } + // Drop anything that could terminate a header line or inject control chars. + $clean = preg_replace('/[\x00-\x1F\x7F]/', '', (string)$value); + return trim($clean); + } + private function getClientIp() { $request = Yii::$app->getRequest(); diff --git a/application/modules/front/controllers/IndexController.php b/application/modules/front/controllers/IndexController.php index 5d22d6c8..4b7f2cc3 100644 --- a/application/modules/front/controllers/IndexController.php +++ b/application/modules/front/controllers/IndexController.php @@ -146,12 +146,21 @@ public function actionContact() { } public function actionFlushCache($key = NULL) { + // Restrict cache management to loopback callers (server-local maintenance scripts). + // Without this guard any anonymous visitor could repeatedly flush the cache + // and cause a stampede on the origin database. + $remoteIp = Yii::$app->getRequest()->getUserIP(); + $allowedIps = ['127.0.0.1', '::1']; + if (!in_array($remoteIp, $allowedIps, true)) { + throw new \yii\web\ForbiddenHttpException('Forbidden.'); + } + if (!Yii::$app->getRequest()->getIsPost()) { + throw new \yii\web\MethodNotAllowedHttpException('POST required.'); + } if (is_null($key)) $success = Yii::$app->cache->flush(); else { $key = rawurldecode($key); $success = Yii::$app->cache->delete($key); - //Yii::log("Attempting to delete key $key", 'info', 'system.web.CController'); - //$success = $key; } $this->view->params['success'] = $success; echo $this->renderPartial('flushcache'); diff --git a/public/processer.php b/public/processer.php index 70e0b7f3..5e2ef8d3 100644 --- a/public/processer.php +++ b/public/processer.php @@ -24,8 +24,14 @@ function getIP() { $errortype = $_POST['type']." ".$_POST['othererror']; $errortext = $_POST['re_additional']; - $email = $_POST['email']; - if (strlen($email) <= 3) $email = $parameters['adminEmail']; + $email = isset($_POST['email']) ? trim((string)$_POST['email']) : ''; + // Reject anything that doesn't look like a valid email address or that + // contains CR/LF (mail header injection). Fall back to adminEmail. + if (strlen($email) <= 3 + || !filter_var($email, FILTER_VALIDATE_EMAIL) + || preg_match('/[\r\n]/', $email)) { + $email = $parameters['adminEmail']; + } $resp = recaptcha_check_answer( $privatekey, diff --git a/public/search_redirect.php b/public/search_redirect.php index 561ec51d..5499fc15 100644 --- a/public/search_redirect.php +++ b/public/search_redirect.php @@ -22,11 +22,19 @@ function url_encode($query) { elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) $IP=$_SERVER['HTTP_X_FORWARDED_FOR']; else $IP=$_SERVER['REMOTE_ADDR']; - $con = mysqli_connect($parameters['searchdb_host'], $parameters['searchdb_username'], $parameters['searchdb_password']) or die(mysqli_error($con)); - mysqli_select_db($parameters['searchdb_name']) or die(mysqli_error($con)); - $query = "INSERT into didyoumean (query, suggestion, IP) values ('".addslashes($_GET['old'])."','".addslashes($_GET['query'])."','".$IP."')"; - mysqli_query($con, $query) or die(mysqli_error($con).$query); - mysqli_close($con); + $con = mysqli_connect($parameters['searchdb_host'], $parameters['searchdb_username'], $parameters['searchdb_password'], $parameters['searchdb_name']); + if ($con) { + $stmt = mysqli_prepare($con, "INSERT into didyoumean (query, suggestion, IP) values (?, ?, ?)"); + if ($stmt) { + $old = isset($_GET['old']) ? (string)$_GET['old'] : ''; + $q = isset($_GET['query']) ? (string)$_GET['query'] : ''; + $ip = (string)$IP; + mysqli_stmt_bind_param($stmt, 'sss', $old, $q, $ip); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + } + mysqli_close($con); + } } header('Location: /search/'.addslashes(url_encode(trim($_GET['query'])))); diff --git a/public/share.php b/public/share.php index 3cf2d341..da0080bc 100644 --- a/public/share.php +++ b/public/share.php @@ -1,6 +1,17 @@

SHARE THIS HADITH

@@ -15,7 +26,7 @@