소스 검색

Security patch

ryp 3 주 전
부모
커밋
cad6c10817

+ 1 - 0
app/controllers/certs_manage.php

@@ -33,6 +33,7 @@
 	}
 
 	if ($_POST['certs'] != '') {
+		crsf_defend();
 		$certs = DB::escape($_POST['certs']);
 		$username = $myUser['username'];
 		if (!in_array($certs, explode(',', $myUser['certslist'])))

+ 1 - 0
app/controllers/click_zan.php

@@ -17,6 +17,7 @@
 	if (!validateZan()) {
 		die('<div class="text-danger">failed</div>');
 	}
+	crsf_defend();
 	if ($myUser == null) {
 		die('<div class="text-danger">please <a href="'.HTML::url('/login').'">log in</a></div>');
 	}

+ 1 - 1
app/controllers/group_info.php

@@ -6,7 +6,7 @@
 <?php if (validateUsername($username) && ($user = queryUser($username))): ?>
 	<?php echoUOJPageHeader($user['username'] . ' - ' . '组信息') ?>
 	<?php
-	$email = $user['email'];
+	$email = HTML::escape($user['email']);
 	$esc_motto = HTML::escape($user['motto']);
 	$quota = printSize(queryGroupQuota($user));
 	$problemquota = queryGroupProblemQuota($user);

+ 1 - 1
app/controllers/groups_manage.php

@@ -81,7 +81,7 @@
 					}
 					else {
 						$grp = queryUser($myUser['childgroup']);
-						$grpnick = $grp['email'];
+						$grpnick = HTML::escape($grp['email']);
 						$grpname = $grp['username'];
 						echo "<div class='row mt-2'>";
 						echo "<div class='col-sm-6'><h4>$grpnick</h4></div>";

+ 3 - 3
app/controllers/index.php

@@ -696,7 +696,7 @@ $gk28daysLeft  = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
                                 <?= getUserLink($user['username']) ?>
                             </div>
                             <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
+                                <?= HTML::escape($user['motto']) ?>
                             </div>
                         </div>
                     </div>
@@ -727,7 +727,7 @@ $gk28daysLeft  = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
                                 </strong>
                             </div>
                             <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
+                                <?= HTML::escape($user['motto']) ?>
                             </div>
                         </div>
                     </div>
@@ -754,7 +754,7 @@ $gk28daysLeft  = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
                                 </strong>
                             </div>
                             <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
+                                <?= HTML::escape($user['motto']) ?>
                             </div>
                         </div>
                     </div>

+ 29 - 9
app/controllers/judge/submit.php

@@ -6,6 +6,17 @@
 		become404Page();
 	}
 	
+	// $_POST['result'] is attacker-controllable whenever the judger password is
+	// weak or leaked (the repo used to ship a placeholder), so decode it once
+	// and refuse anything that is not an object.
+	function judgeResultFromPost() {
+		$result = json_decode($_POST['result'] ?? '', true);
+		if (!is_array($result)) {
+			die("Wow! hacker! T_T....");
+		}
+		return $result;
+	}
+
 	function submissionJudged() {
 		$submission = DB::selectFirst("select submitter, status, content, result, problem_id from submissions where id = {$_POST['id']}");
 		if ($submission == null) {
@@ -18,7 +29,7 @@
 		
 		if (isset($content['first_test_config'])) {
 			$result = json_decode($submission['result'], true);
-			$result['final_result'] = json_decode($_POST['result'], true);
+			$result['final_result'] = judgeResultFromPost();
 			$result['final_result']['details'] = uojTextEncode($result['final_result']['details']);
 			$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
 			
@@ -29,13 +40,20 @@
 			
 			DB::update("update submissions set status = 'Judged', result = '$esc_result', content = '$esc_content' where id = {$_POST['id']}");
 		} else {
-			$result = json_decode($_POST['result'], true);
+			$result = judgeResultFromPost();
 			$result['details'] = uojTextEncode($result['details']);
 			$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
+			// status/result_error are varchar and score/used_* are int, so
+			// escape the former and cast the latter.
+			$status = DB::escape($result['status']);
 			if (isset($result["error"])) {
-				DB::update("update submissions set status = '{$result['status']}', result_error = '{$result['error']}', result = '$esc_result', score = null, used_time = null, used_memory = null where id = {$_POST['id']}");
+				$error = DB::escape($result['error']);
+				DB::update("update submissions set status = '$status', result_error = '$error', result = '$esc_result', score = null, used_time = null, used_memory = null where id = {$_POST['id']}");
 			} else {
-				DB::update("update submissions set status = '{$result['status']}', result_error = null, result = '$esc_result', score = {$result['score']}, used_time = {$result['time']}, used_memory = {$result['memory']} where id = {$_POST['id']}");
+				$score = (int)$result['score'];
+				$time = (int)$result['time'];
+				$memory = (int)$result['memory'];
+				DB::update("update submissions set status = '$status', result_error = null, result = '$esc_result', score = $score, used_time = $time, used_memory = $memory where id = {$_POST['id']}");
 			}
 			
 			if (isset($content['final_test_config'])) {
@@ -60,21 +78,23 @@
 			return;
 		}
 		$content = json_decode($submission['content'], true);
-		$result = json_decode($_POST['result'], true);
+		$result = judgeResultFromPost();
 		$result['details'] = uojTextEncode($result['details']);
 		$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
+		$status = DB::escape($result['status']);
 		if (isset($result["error"])) {
-			DB::update("update custom_test_submissions set status = '{$result['status']}', result = '$esc_result' where id = {$_POST['id']}");
+			DB::update("update custom_test_submissions set status = '$status', result = '$esc_result' where id = {$_POST['id']}");
 		} else {
-			DB::update("update custom_test_submissions set status = '{$result['status']}', result = '$esc_result' where id = {$_POST['id']}");
+			DB::update("update custom_test_submissions set status = '$status', result = '$esc_result' where id = {$_POST['id']}");
 		}
 		DB::update("update custom_test_submissions set status_details = '' where id = {$_POST['id']}");
 	}
 	
 	function hackJudged() {
-		$result = json_decode($_POST['result'], true);
+		$result = judgeResultFromPost();
 		$esc_details = DB::escape(uojTextEncode($result['details']));
-		$ok = DB::update("update hacks set success = {$result['score']}, details = '$esc_details' where id = {$_POST['id']}");
+		$success = (int)$result['score'];
+		$ok = DB::update("update hacks set success = $success, details = '$esc_details' where id = {$_POST['id']}");
 		
 		if ($ok) {
 			list($hack_input) = DB::fetch(DB::query("select input from hacks where id = {$_POST['id']}"), MYSQLI_NUM);

+ 3 - 3
app/controllers/problem.php

@@ -217,7 +217,7 @@ EOD
 <?php if ($contest): ?>
 <div class="page-header row">
 	<h1 class="col-md-3 text-left"><small><?= $contest['name'] ?></small></h1>
-	<h1 class="col-md-7 text-center"><?= $problem_letter ?>. <?= $problem['title'] ?></h1>
+	<h1 class="col-md-7 text-center"><?= $problem_letter ?>. <?= HTML::escape($problem['title']) ?></h1>
 	<div class="col-md-2 text-right" id="contest-countdown"></div>
 </div>
 <?php if ($contest['cur_progress'] <= CONTEST_IN_PROGRESS): ?>
@@ -227,7 +227,7 @@ $('#contest-countdown').countdown(<?= $contest['end_time']->getTimestamp() - UOJ
 </script>
 <?php endif ?>
 <?php else: ?>
-<h1 class="page-header text-center">#<?= $problem['id']?>. <?= $problem['title'] ?></h1>
+<h1 class="page-header text-center">#<?= $problem['id']?>. <?= HTML::escape($problem['title']) ?></h1>
 <?php endif ?>
 
 <div class="row">
@@ -292,7 +292,7 @@ $('#contest-countdown').countdown(<?= $contest['end_time']->getTimestamp() - UOJ
 							<?php if ($problem['ctrl'] == ''): ?>
 							<span>默认</span>
 							<?php else: ?>
-							<a href="<?= HTML::url('/user/profile/' . $problem['ctrl']) ?>"><?= queryUser($problem['ctrl'])['email'] ?></a>
+							<a href="<?= HTML::url('/user/profile/' . $problem['ctrl']) ?>"><?= HTML::escape(queryUser($problem['ctrl'])['email']) ?></a>
 							<?php endif ?>
 						</li>
 						<li class="d-flex justify-content-between">

+ 1 - 1
app/controllers/problem_data_manage.php

@@ -618,7 +618,7 @@ EOD
 	$REQUIRE_LIB['dialog'] = '';
 ?>
 <?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 数据 - 题目管理') ?>
-<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=HTML::escape($problem['title'])?> 管理</h1>
 <ul class="nav nav-tabs" role="tablist">
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>

+ 1 - 1
app/controllers/problem_data_manage_back.php

@@ -618,7 +618,7 @@ EOD
 	$REQUIRE_LIB['dialog'] = '';
 ?>
 <?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 数据 - 题目管理') ?>
-<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=HTML::escape($problem['title'])?> 管理</h1>
 <ul class="nav nav-tabs" role="tablist">
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>

+ 1 - 1
app/controllers/problem_managers_manage.php

@@ -28,7 +28,7 @@
 	$managers_form->runAtServer();
 ?>
 <?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 管理者 - 题目管理') ?>
-<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=HTML::escape($problem['title'])?> 管理</h1>
 <ul class="nav nav-tabs" role="tablist">
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
 	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>

+ 24 - 7
app/controllers/problem_set.php

@@ -4,6 +4,9 @@
 	requirePHPLib('data');
 	
 	if ($_POST['migrate_problem_submit'] == 'submit') {
+		if ($myUser == null) {
+			redirectToLogin();
+		}
 		$group = queryUser(DB::escape($_POST['target_group']));
 		$start = $_POST['start_pid'];
 		$end = $_POST['end_pid'];
@@ -13,8 +16,8 @@
 			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
 		}
 
-		if (!group) {
-			$errmsg = "Error: 未找到组 {$_POST['target_group']}";
+		if (!$group) {
+			$errmsg = "Error: 未找到组 " . HTML::escape($_POST['target_group']);
 			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
 		}
 
@@ -37,17 +40,31 @@
 	}
 
 	if ($_POST['copy_problem_submit'] == 'submit') {
-		$problems = explode(',', $_POST['copy_problem_list']);
+		if ($myUser == null) {
+			redirectToLogin();
+		}
+		$problems = explode(',', $_POST['copy_problem_list'] ?? '');
 		if (count($problems) > 10) {
 			$errmsg = "Error: 你不能一次复制超过 10 个题目";
 			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
 		}
 
-		foreach ($problems as $pid)
+		// Every id must be validated *before* it is used: queryProblemBrief()
+		// and queryProblemContent() interpolate it into SQL, and
+		// dataCopyProblemData() interpolates it straight into exec().
+		foreach ($problems as $pid) {
+			if (!validateUInt($pid)) {
+				$errmsg = "Error: 不合法的题目 ID";
+				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+			}
+		}
+
+		foreach ($problems as $pid) {
 			if (!hasProblemCtrlPermission($myUser, queryProblemBrief($pid))) {
-				$errmsg = "Error: 你对题目 $i 没有控制权";
+				$errmsg = "Error: 你对题目 $pid 没有控制权";
 				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
 			}
+		}
 		foreach ($problems as $pid) {
 			$p = queryProblemBrief($pid);
 			$title = DB::escape($p['title']);
@@ -77,8 +94,8 @@
 			if ($problem['is_hidden']) {
 				echo ' <span class="text-danger">[隐藏]</span> ';	
 			}
-			echo '<a href="/problem/', $problem['id'], '">', $problem['title'], '</a>';
-			echo '<td><a href="/user/profile/', $problem['ctrl'], '">', queryUser($problem['ctrl'])['email'], ' (', $problem['ctrl'], ')</a></td>';
+			echo '<a href="/problem/', $problem['id'], '">', HTML::escape($problem['title']), '</a>';
+			echo '<td><a href="/user/profile/', $problem['ctrl'], '">', HTML::escape(queryUser($problem['ctrl'])['email']), ' (', $problem['ctrl'], ')</a></td>';
 			if (isset($_COOKIE['show_tags_mode'])) {
 				foreach (queryProblemTags($problem['id']) as $tag) {
 					echo '<a class="uoj-problem-tag">', '<span class="badge badge-pill badge-secondary">', HTML::escape($tag), '</span>', '</a>';

+ 1 - 1
app/controllers/problem_statement_manage.php

@@ -47,7 +47,7 @@
 	$problem_editor->runAtServer();
 ?>
 <?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 编辑 - 题目管理') ?>
-<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=HTML::escape($problem['title'])?> 管理</h1>
 <ul class="nav nav-tabs" role="tablist">
 	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
 	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>

+ 1 - 1
app/controllers/problem_statistics.php

@@ -59,7 +59,7 @@
 ?>
 <?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - ' . UOJLocale::get('problems::statistics')) ?>
 
-<h1 class="page-header text-center"><?= $problem['title'] ?> <?= UOJLocale::get('problems::statistics') ?></h1>
+<h1 class="page-header text-center"><?= HTML::escape($problem['title']) ?> <?= UOJLocale::get('problems::statistics') ?></h1>
 
 <?php if ($contest && !hasContestPermission($myUser, $contest) && $contest['cur_progress'] <= CONTEST_IN_PROGRESS): ?>
 <h2 class="text-center text-muted">比赛尚未结束</h2>

+ 1 - 1
app/controllers/rps.php

@@ -8,7 +8,7 @@
 		echo '<tr class="text-center">';
 		echo '<td>' . $problem['pid'] . '</td>';
 		echo '<td class="text-left">';
-		echo '<a href="/rp/', $problem['pid'], '">', $problem['title'], '</a>';
+		echo '<a href="/rp/', $problem['pid'], '">', HTML::escape($problem['title']), '</a>';
 		echo '</td></tr>';
 	}
 	

+ 2 - 2
app/controllers/user_info.php

@@ -13,9 +13,9 @@
 	$groups = explode(',', $user['extgroups']);
 	$extgroups = '';
 	foreach ($groups as $grp) {
-		$grpname = queryUser($grp)['email'];
+		$grpname = HTML::escape(queryUser($grp)['email']);
 		if ($grpname == '') $grpname = '无昵称';
-		$extgroups .= "<a href=\"/user/profile/{$grp}\">$grpname ($grp)</a>";
+		$extgroups .= "<a href=\"/user/profile/{$grp}\">$grpname (" . HTML::escape($grp) . ")</a>";
 		$extgroups .= '<br/>';
 	}
 

+ 3 - 1
app/controllers/user_msg.php

@@ -99,6 +99,7 @@ EOD;
 */
 
 	if (isset($_POST['user_msg'])) {
+		crsf_defend();
 		die(handleMsgPost());
 	} elseif (isset($_GET['getConversations'])) {
 		die(getConversations());
@@ -209,7 +210,8 @@ function submitMessagePost(conversationName) {
 		$.post('/user/msg', {
 				user_msg : 1,
 				receiver : conversationName,
-				message : $('#input-message').val()
+				message : $('#input-message').val(),
+				_token : uojCsrfToken
     }, function(msg) {
 				$('#input-message').val("");
 		});

+ 22 - 7
app/libs/uoj-data-lib.php

@@ -2,11 +2,23 @@
 	// Actually, these things should be done by main_judger so that the code would be much simpler.
 	// However, this lib exists due to some history issues.
 	
+	function dataCheckId($id, $fn) {
+		// Accepts int (mysqli_insert_id) or numeric string (mysqli result).
+		if (!preg_match('/^[0-9]{1,9}$/', (string)$id)) {
+			error_log("$fn: invalid problem id " . var_export($id, true));
+			return false;
+		}
+		return true;
+	}
+
 	function dataNewProblem($id) {
+		if (!dataCheckId($id, __FUNCTION__)) {
+			return;
+		}
 		mkdir("/var/uoj_data/upload/$id");
 		mkdir("/var/uoj_data/$id");
-		
-		exec("cd /var/uoj_data; rm $id.zip; zip $id.zip $id -r -q");
+
+		exec("cd /var/uoj_data; rm " . escapeshellarg("$id.zip") . "; zip " . escapeshellarg("$id.zip") . ' ' . escapeshellarg($id) . " -r -q");
 	}
 
 	class UOJProblemConfException extends Exception {
@@ -21,9 +33,12 @@
 	}
 
 	function dataCopyProblemData($src, $dst) {
-		exec("rm -r /var/uoj_data/upload/$dst");
-		exec("cp -r /var/uoj_data/$src /var/uoj_data/$dst");
-		exec("cd /var/uoj_data; zip $dst.zip $dst -r -q");
+		if (!dataCheckId($src, __FUNCTION__) || !dataCheckId($dst, __FUNCTION__)) {
+			return;
+		}
+		exec("rm -r " . escapeshellarg("/var/uoj_data/upload/$dst"));
+		exec("cp -r " . escapeshellarg("/var/uoj_data/$src") . ' ' . escapeshellarg("/var/uoj_data/$dst"));
+		exec("cd /var/uoj_data; zip " . escapeshellarg("$dst.zip") . ' ' . escapeshellarg($dst) . " -r -q");
 	}
 	
 	function dataClearProblemData($problem) {
@@ -33,8 +48,8 @@
 			return "invalid problem id";
 		}
 		
-		exec("rm /var/uoj_data/upload/$id -r");
-		exec("rm /var/uoj_data/$id -r");
+		exec("rm -r " . escapeshellarg("/var/uoj_data/upload/$id"));
+		exec("rm -r " . escapeshellarg("/var/uoj_data/$id"));
 		dataNewProblem($id);
 	}
 	

+ 13 - 11
app/libs/uoj-html-lib.php

@@ -91,22 +91,22 @@ function getUserLink($username, $rating = null, $certs = null) {
 
 function getRemoteProblemLink($rpid) {
 	$rp = queryRemoteProblemBrief($rpid);
-	return '<a href="/rp/' . $rpid . '">' . $rp['title'] . '</a>';
+	return '<a href="/rp/' . $rpid . '">' . HTML::escape($rp['title']) . '</a>';
 }
 
 function getProblemLink($problem, $problem_title = '!title_only') {
 	if ($problem_title == '!title_only') {
-		$problem_title = $problem['title'];
+		$problem_title = HTML::escape($problem['title']);
 	} elseif ($problem_title == '!id_and_title') {
-		$problem_title = "#${problem['id']}. ${problem['title']}";
+		$problem_title = "#${problem['id']}. " . HTML::escape($problem['title']);
 	}
 	return '<a href="/problem/'.$problem['id'].'">'.$problem_title.'</a>';
 }
 function getContestProblemLink($problem, $contest_id, $problem_title = '!title_only') {
 	if ($problem_title == '!title_only') {
-		$problem_title = $problem['title'];
+		$problem_title = HTML::escape($problem['title']);
 	} elseif ($problem_title == '!id_and_title') {
-		$problem_title = "#{$problem['id']}. {$problem['title']}";
+		$problem_title = "#{$problem['id']}. " . HTML::escape($problem['title']);
 	}
 	return '<a href="/contest/'.$contest_id.'/problem/'.$problem['id'].'">'.$problem_title.'</a>';
 }
@@ -429,7 +429,7 @@ function echoBelongGroupList($user) {
 	echoShortList('<tr><th>组名</th><th>显示名</th><th>题目</th></tr>', queryUserExtgroups($user), function($grp) {
 		$qp = queryUser($grp);
 		$link = getUserLink($qp['username']);
-		$name = $qp['email'];
+		$name = HTML::escape($qp['email']);
 		echo <<<HTML
 <tr>
 	<td>$link</td>
@@ -462,7 +462,7 @@ function echoGroupProblemList($grp) {
 		function($pid) use ($grp) {
 			$problem = queryProblemBrief($pid);
 			$pid = $pid;
-			$title = $problem['title'];
+			$title = HTML::escape($problem['title']);
 			if ($problem['ctrl'] && $problem['ctrl'] == $grp['username']) $adminp = '<b>是</b>';
 			else $adminp = '否';
 			$size = printSize(queryProblemSize($pid));
@@ -480,7 +480,7 @@ function echoGroupProblemSizeList($grp) {
 		function($pid) {
 			$problem = queryProblemBrief($pid);
 			$pid = $pid;
-			$title = $problem['title'];
+			$title = HTML::escape($problem['title']);
 			$size = printSize(queryProblemSize($pid));
 			echo <<<HTML
 <tr><td>$pid</td>
@@ -493,7 +493,7 @@ function echoControlGroupList($user) {
 	echoShortList('<tr><th>组名</th><th>显示名</th><th>管理</th></tr>', queryUserCtrlgroups($user), function($grp) {
 		$qp = queryUser($grp);
 		$link = getUserLink($qp['username']);
-		$name = $qp['email'];
+		$name = HTML::escape($qp['email']);
 		echo <<<HTML
 <tr>
 	<td>$link</td>
@@ -530,14 +530,16 @@ HTML;
 
 function echoCertsList($user) {
 	echoShortList('<tr><th>勋章</th><th>展示</th></tr>', explode(',', $user['certslist']), function($certs) {
+		$token = HTML::hiddenToken();
 		$col = explode('#', $certs)[0];
-		$text = explode('#', $certs)[1];
+		$text = HTML::escape(explode('#', $certs)[1]);
 		$cert = '<span class="uoj-certs uoj-certs-' . $col . '">' . $text . '</span>';
 		echo <<<HTML
 <tr>
 	<td>$cert</td>
 	<td>
 		<form action="/certs-manage" method="post">
+			{$token}
 			<input type="hidden" name="certs" value="$certs">
 			<button type="submit" class="btn btn-primary">展示这个</button>
 		</form>
@@ -1219,7 +1221,7 @@ function echoBlogCard($blog) {
 	$poster = queryUser($blog['poster']);
 	$userlink = getUserLink($blog['poster']);
 	$posttime = $blog['post_time'];
-	$usermotto = $poster['motto'];
+	$usermotto = HTML::escape($poster['motto']);
 	$zanblock = getClickZanBlock('B', $blog['id'], $blog['zan']);
 	$avatar = "https://q.qlogo.cn/g?b=qq&nk=" . $poster['qq'] . "&s=100";
 

+ 1 - 1
app/models/HTML.php

@@ -2,7 +2,7 @@
 
 class HTML {
 	public static function escape($str) {
-		return htmlspecialchars($str);
+		return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
 	}
 	public static function stripTags($str) {
 		return strip_tags($str);

+ 1 - 1
app/views/blog-preview.php

@@ -34,7 +34,7 @@
 					<span class="text-muted"><?= $blog['post_time'] ?></span>
 				</div>
 				<div class="d-flex align-items-end">
-					<span><?= $poster['motto'] ?></span>
+					<span><?= HTML::escape($poster['motto']) ?></span>
 				</div>
 			</div>
 		</div>

+ 2 - 2
app/views/page-header.php

@@ -39,7 +39,7 @@
 		<?php endif ?>
 		<title><?= isset($PageTitle) ? $PageTitle : UOJConfig::$data['profile']['oj-name-short'] ?> - <?= $PageMainTitle ?></title>
 		
-		<script type="text/javascript">uojHome = '<?= HTML::url('/') ?>'</script>
+		<script type="text/javascript">uojHome = '<?= HTML::url('/') ?>'; uojCsrfToken = '<?= crsf_token() ?>';</script>
 
 		<!-- Bootstrap core CSS -->
 		<?= HTML::css_link('/css/bootstrap.min.css?v=2019.5.31') ?>
@@ -79,7 +79,7 @@
 		<?= HTML::js_src('/js/color-converter.min.js') ?>
 		
 		<!-- uoj -->
-		<?= HTML::js_src('/js/uoj.js?v=2026.09.06') ?>
+		<?= HTML::js_src('/js/uoj.js?v=2026.09.25') ?>
 		
 		<!-- readmore -->
 		<?= HTML::js_src('/js/readmore/readmore.min.js') ?>

+ 2 - 1
js/uoj.js

@@ -331,7 +331,8 @@ function click_zan(zan_id, zan_type, zan_delta, node) {
 	$.post(zan_link + '/click-zan', {
 		id : zan_id,
 		delta : zan_delta,
-		type : zan_type
+		type : zan_type,
+		_token : uojCsrfToken
 	}, function(ret) {
 		$(loading_node).replaceWith($(ret).click_zan_block());
 	}).fail(function() {