Fix multiple security issues in legacy endpoints#175
Conversation
- 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>
|
Closing — no further changes will be made to this fork. |
hasankhan
left a comment
There was a problem hiding this comment.
Adversarial review — these are good fixes overall, but several have gaps that mean the fix is incomplete or the endpoint is broken. Numbered to match the PR description.
3. actionFlushCache — likely broken by this change
Two blockers:
-
Yii CSRF will reject every call.
IndexControlleronly disables CSRF forajaxhadithcount(beforeActionat line ~41);flush-cacheis in the cache filter'sexceptlist but not exempted from CSRF. With the newgetIsPost()requirement, a loopback maintenance script doingcurl -X POST http://localhost/yiiadmin/flushcachehas no CSRF token and will get a 400. The endpoint is effectively unreachable after this PR. Add tobeforeAction:if ($action->id == 'flush-cache') { $this->enableCsrfValidation = false; }
…or keep it GET (the action is idempotent; the IP allow-list is the real defense).
-
getUserIP()isREMOTE_ADDR, which is not loopback behind a reverse proxy. If Yii is fronted by nginx/Apache on the same host and PHP-FPM gets the real client IP forwarded (e.g.set_real_ip_from/RemoteIPHeader),REMOTE_ADDRwill be the public client IP and the loopback check fails for legitimate local callers. Conversely, if Yii'strustedHostsis ever configured to trustX-Forwarded-For, an attacker can spoof127.0.0.1. Worth either (a) reading$_SERVER['REMOTE_ADDR']directly and documenting the deployment assumption, or (b) gating on a shared secret header instead of IP. As written this is fragile.
Please also verify manually post-merge that curl -X POST http://127.0.0.1/yiiadmin/flushcache actually returns 200 — I'm fairly sure it currently won't.
2. share.php — regex allows things you probably didn't intend
- The character class includes
:and?, so/foo:bar?x=1is accepted. For Facebook (?u=https://sunnah.com<link>) any?or&in$linkcollapses into the outeru=query string and corrupts the share URL. Consider restricting to^/[A-Za-z0-9_\-./]*$and stripping query/fragment, since real hadith permalinks never need them. strpos($rawLink, '//') !== 0only catches a leading double slash. The regex itself permits//anywhere, but since the value is appended afterhttps://sunnah.com, the only dangerous case is//evil.com, which is caught — so this is fine, just worth a comment noting why.nl2br(htmlspecialchars($hadithText))is rendered into the page but$hadithTextis otherwise unconstrained — fine for the preview, but notehadithTextis alsorawurlencoded intohttps://twitter.com/intent/tweet?text=…with no length cap. Twitter will truncate; not a security issue, flagging for awareness.
4. ElasticConnection::sanitizeHeaderValue — fix is correct but incomplete
- No length cap. A 1 MB
User-Agentwill be cleaned and then sent verbatim to ES on every request. Suggestmb_substr($clean, 0, 512)or similar. getClientIp()(unchanged) trustsCF-Connecting-IPandX-Forwarded-Forfrom the request unconditionally — if the app is ever exposed without Cloudflare in front (staging, direct origin hit), any client can set these. Out of scope for this PR but worth a follow-up issue; sanitization here mitigates the smuggling vector but not the spoofing one.
5. processer.php — only email is validated
Reply-To is fixed, but $_POST['type'], $_POST['othererror'], $_POST['re_additional'], and $_POST['urn'] flow into $subject and $fullString. $subject = "[Error Report] URN ".$urn; — $urn from POST goes straight into the Subject header. PEAR Mail will happily pass a CRLF in the subject to the SMTP DATA and inject headers. Please also validate $urn (e.g. ctype_digit) and strip CR/LF from $subject defensively. Otherwise this fix moves the injection point one field over.
1. search_redirect.php — two leftover issues
- Line 32 (unchanged):
header('Location: /search/'.addslashes(url_encode(trim($_GET['query']))));—addslasheson a Location header is meaningless for HTTP and does nothing for CRLF.url_encode()(the local function) doesrawurlencodeat the end which does encode\r\n, so it happens to be safe today, but theaddslashesis misleading dead code andurl_encodeis doing the actual escaping. Worth removing theaddslashes. - IP value (
HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR) is fully attacker-controlled and now bound as a string. Not SQLi anymore, but the column may overflow / store junk. Considerfilter_var($IP, FILTER_VALIDATE_IP)with a fallback toREMOTE_ADDR. - Silently swallowing connect/prepare failures (no logging) makes the original
or die(mysqli_error($con))behavior — broken as it was — strictly less debuggable. Considererror_log()on failure.
General
- Per repo conventions there are no working tests, but at minimum each of these would benefit from a manual repro recipe in the PR description (curl one-liner per fix) so a reviewer / future-you can re-verify. I'd specifically ask for one for
flushcachebecause I think it's broken. - No changes to
public/report.php— is it intentional that it was reviewed and found clean, or just not yet looked at? Worth a note.
Happy to re-review once the flush-cache CSRF/POST issue and the subject-header injection in processer.php are addressed.
Reverting the change to public/search_redirect.php from the security fixes PR per reviewer feedback. Will address separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes several security issues found via a code review of the legacy non-Yii endpoints and a few Yii components.
1. SQL injection in
public/search_redirect.php(Critical)X-Forwarded-For/HTTP_CLIENT_IPflowed unescaped into anINSERTon the search DB, andmysqli_errorwas echoed back on failure (error-based exfiltration). Replaced with amysqli_prepare+bind_paramcall and removed the error echo.2. Reflected XSS in
public/share.php(High)\['link']was echoed unescaped insidehref="..."attributes for the Facebook / WhatsApp / Telegram share buttons. An attacker-controlled form post could break out of the attribute and inject event handlers. Now HTML-escaped withENT_QUOTESand constrained to a site-relative path via regex.3. Unauthenticated cache flush in
IndexController::actionFlushCache(High)The route
GET /yiiadmin/flushcachewas publicly reachable with no auth, allowing any visitor to repeatedly flush Memcached (cache-stampede DoS on the origin DB) or delete arbitrary cache keys. Now requires POST and a loopback caller (127.0.0.1/::1), matching how server-local maintenance scripts would invoke it.4. CRLF / header injection in
ElasticConnection(Medium)User-Agentand the forwarded client IP (taken from request headers) were concatenated into outbound HTTP headers joined with\r\n. A craftedUser-Agentcould inject additional headers or smuggle a second request into the Elasticsearch connection. Added asanitizeHeaderValuehelper that strips\x00-\x1Fand\x7F.5. Mail header injection in
public/processer.php(Medium)\['email']was placed into theReply-Toheader with no validation, enablingBcc:/Content-Type:injection and turning the form into an open relay using the SES credentials. Now validated withFILTER_VALIDATE_EMAILand rejected if it contains CR/LF, with fallback toadminEmail.All five files pass
php -l.