【PHP】RPGゲームを作る5(ログイン画面)

ゲームのログイン画面を作ります。SQLインジェクション対策と多重ログイン対策を行っています。ブルートフォース攻撃対策は、apache側のmod_dosdetectorあたりで行うのがいいのではないかと思い、今回は実装していません。

環境

 OS ubuntu22.04 
 Webサーバ apache2.4.52
   ※メール送信の為にsendmailパッケージを有効にしています
(自身のIP 192.168.19.128、公開フォルダ /var/www/html/php/rpg)
 PHP 8.1.2-1ubuntu2.14
 DB MySQL Ver 8.0.34-ubuntu0.22.04.1

ログイン画面を作る

次のPHPプログラムを作成し、アップロード後パーミッションを755にしました。

<?PHP
//定数

// MySQL接続情報
const SERVER_NAME = '192.168.19.128'; // データベースのホスト名
const DB_USER = '********'; // データベースのユーザー名
const DB_PASSWORD = '********'; // データベースのパスワード
const DB_NAME = '****'; // 使用するデータベース名

const DEF_POST = 'POST';
const DEF_DO = 'do';
const DEF_PIN = 'pin';
const DEF_USERNAME = 'user_name';
const DEF_USERPASS = 'user_pass';

const DEF_LOGIN = '_LOGIN_';
const DEF_START = '_START_';
const DEF_STATUS_REGEST ='_REGEST_';
const DEF_RPG_ACTION ='./rpg.php'; //ゲーム画面のPHP

?>
<!DOCTYPE html>
<html>
<head></head>
<body>
<?PHP

//db読込
// MySQLサーバーへの接続
$conn = new mysqli(SERVER_NAME, DB_USER, DB_PASSWORD, DB_NAME);

// 接続エラーの確認
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

//ヘッダー
header_page($conn);

//メイン処理
if ($_SERVER['REQUEST_METHOD'] === DEF_POST 
	&& isset($_POST[DEF_DO])
	&& $_POST[DEF_DO] === DEF_LOGIN
	&& isset($_POST[DEF_USERNAME])
	&& isset($_POST[DEF_USERPASS])){
	
	// DB問い合わせ後ゲーム画面へリダイレクト
	after_login_page($conn,$_POST);
	
}else {
	//ログイン画面 
	login_page();

}

//フッター
footer_page($conn);
$conn->close();

?>
</body>
</html>
<?PHP //PHP function  ****************************************************************
// header page************************************************************************
function header_page($conn){
	echo "<h1>ログイン画面</h1>";
	echo '<form action="" name="myform" id="myform" method="'.DEF_POST.'">';
	echo '<input type="hidden" name="'.DEF_DO.'" id="'.DEF_DO.'" value=""><br>';
}

// footer page**************************************************************************
function footer_page($conn){
	echo '</form>';
	echo '<hr><A href="./login.php">reload</a><br>';
}

//パスワードジェネレータ****************************************************************
function pass_gene($user_name,$password){
	$pepper = "kosyou_";//環境に合わせて変えてください
	$gene_pass = hash('sha256', $user_name . $pepper . $password);
	return $gene_pass;
}
// login page************************************************************************
function login_page(){
?>
<script type="text/javascript">
<!--
function login_button(){
	if(	document.getElementById("<?PHP echo DEF_USERNAME; ?>").value === ""){
		alert("ユーザ名を入力してください");
		exit;
	}
	if(	document.getElementById("<?PHP echo DEF_USERPASS; ?>").value === ""){
		alert("パスワードを入力してください");
		exit;
	}
	document.getElementById("<?PHP echo DEF_DO; ?>").value="<?PHP echo DEF_LOGIN; ?>";
	document.myform.submit();
}
//-->
</script>
<?PHP
	echo 'user_name<input type="text" name="'.DEF_USERNAME.'" id="'.DEF_USERNAME.'" value="" autocomplete="off"><br>';
	echo 'password<input type="password" name="'.DEF_USERPASS.'" id="'.DEF_USERPASS.'" value="" autocomplete="off"><br>';
	echo '<input type="submit" value="LOGIN" id="login_b" onclick="login_button();"><br>';
}

// after login  page************************************************************************
function after_login_page($conn,$PO){
	
	$user_name = $PO[DEF_USERNAME];
	// パスワードのハッシュ値を生成する
	$hashed_password = pass_gene($user_name, $PO[DEF_USERPASS]);
	
	$stmt = $conn->prepare("SELECT * FROM user_data WHERE username=? and password=? and status='".DEF_STATUS_REGEST."'");
	$stmt->bind_param("ss", $user_name,$hashed_password);
	$stmt->execute();
	$result = $stmt->get_result();
	
	if ($row = $result->fetch_assoc()) {
		// IDを取得
		$user_id = $row['user_no'];
		//正常ログイン
		//多重ログインを防ぐためにPINコードを発行する(後勝ち)
		$pin =  strval(mt_rand(100000, 999999));
		$stmt = $conn->prepare("UPDATE user_data SET pin=?,lastmodiffy=CURRENT_TIMESTAMP  WHERE user_no=?");
		$stmt->bind_param("si", $pin, $user_id);
		$stmt->execute();
		echo '<input type="hidden" name="'.DEF_PIN.'" id="'.DEF_PIN.'" value="'.$pin.'">';
?>

<script type="text/javascript">
<!--
function start_button(){
	var myForm = document.getElementById("myform");
	myForm.action = "<?PHP echo DEF_RPG_ACTION; ?>";
	document.getElementById("<?PHP echo DEF_DO; ?>").value="<?PHP echo DEF_START; ?>";
	myForm.submit();
}
start_button();
//-->
</script>
<?PHP
	} else{
		//ログイン失敗
		echo "ユーザ名かパスワードが違います。<br>";
		//ログイン画面 
		login_page();
	}
	$stmt->close();
}

 PINを後勝ち設定にしたのは、正規のユーザがログアウトしないでブラウザを閉じた際に、正規のユーザもログインできなくなるからです。

1 SQLインジェクション対策

  SQLインジェクション対策は、SQLの脆弱性というより、コーディングの問題だと考えています。バインド命令を使えばよいのですから、プリペアドステートメントを使って値をバインドさせるようにしましょう。

//悪い例
$user = "userA";
$pass = "12345";
// SQLの実行
$result = $conn->query("SELECT * FROM user_table WHERE user = '$user' AND pass = '$pass'");
if ($result->num_rows > 0) {
    // ログイン成功
} else {
    // ログイン失敗
}

//プリペアドステートメントを使った例
$user = "userA";
$pass = "12345";
$stmt = $conn->prepare("SELECT * FROM user_table WHERE user =? and password=?");
$stmt->bind_param("ss", $user ,$pass );
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
  //ログイン成功
} else {
  //ログイン失敗
}

 悪い例を使用すると、SQLインジェクション攻撃を受けることになりますので絶対に使用しないでください。
 このような古い攻撃手法が残っているのは、いまだに対策をとっていないシステムが少なからず残っているからです。

2 多重ログイン対策

 ブラウザを二つ以上起動し、通常イベントが起こらない場所でイベントを実行させたり、敵のエンカウントなどランダム要素のあるイベントで有利になるように操作を行う手法です。
 今回はログイン画面でPINを発行し、このPINを持っているブラウザしか操作ができない仕組みにします。
 ログイン画面を通過しないとゲーム画面を開くことができない仕様にし、ログインするたびにPINを上書きする仕組みです。
 今回はPINの発行のみで、PINの確認処理はゲーム画面のPHPに実装予定です。

3 他の不正対策

 セッションジャック対策は、画面に埋め込んだJavascriptを使う手法が多いので、画面に表示される入力文字列はJavascriptを埋め込まれないようにするために、
 $user_name = htmlspecialchars($_POST[‘user_name’], ENT_QUOTES, ‘UTF-8’);
を使って一部の記号をエスケープしています。
 また、GETパラメータによる不正な値の挿入対策として、POSTメソッドしか受け付けないようにしています。

 絶対安全という対策はなかなかできませんが、できるだけ対策を行いましょう。
 (JSのDOMを使った攻撃とか、マクロ対策、JS無効ブラウザ対策、historyback、リバースブルートフォースアタック… あぁ ごめんなさい ゆくゆく考えます)