七月中旬,我離開了 Rosetta.ai。
作為最後幾份工作,我與同事們一起設計了一系列的 PHP 軟體工程師(後端)的題目。其中,實作題的設計是由我所主導,而我個人認為它是我設計過最優秀的題目。
因為該題目已獲公司主管同意已經公佈在 PTT 的 Soft_job 版上,所以這邊寫下當時我設計題目的理念與解析。
註 :雖然 PTT 的討論串到最後演變成薪資之爭模糊焦點有些可惜,不過這並不妨礙這份題目本身的設計。
題目
下列 PHP 程式碼存在一些問題,請嘗試指出這些問題並且重構它。
註:下述程式隱藏了一些不重要的細節(例如資料庫連線、失敗處理等),回答時也可以隱藏實作細節(不一定要精準的使用所有的 PHP 內建函式)
<?php
extract($_POST);
$db = new DB();
$user = $db->query("SELECT * FROM users WHERE username=$username AND password=$password"); // query from DB
echo $user ? 'Login Access' : 'Login Failed';
問題剖析
基礎:extract 函式
extract
函式是一個 PHP 內 建函式,它可以將陣列中的鍵值對轉為變數:
$foo = ['a' => 1, 'b' => 2];
extract($foo);
echo $a; // 1
echo $b; // 2
在某些時候,這是一個方便的函式,因為它可以讓我們省去指派變數的冗餘程式碼。
只不過,在預設的情況下,如果跟既有的變數衝突,extract
函式會覆蓋掉已經定義的變數(由第二個參數 flag
決定,預設是 EXTRA_OVERWRITE
)。
不要對不可信的資料使用 extract()
,像是來自用戶的輸入 (例如 $_GET
、$_FILES
)。
extract
最大的安全風險是拿來汙染超全域變數,例如 $_SESSION
。一般來說用戶不應該能夠擅自修改 $_SESSION
的內容,但利用 extract
函式就可以做到這一點。
<?php
if (!session_id()) { session_start(); }
extract($_GET);
if (!($_SESSION['user'] ?? null)) { echo 'You shall not pass!'; exit; }
echo 'Welcome User!'
假設用戶尚未登入 $_SESSION['user']
的值為 null
,上述的程式應該會直接顯示 You shall not pass!
並且退出。
然而,因為使用了 extract($_GET)
,只要我們輸入 /?_SESSION[user]=true
就可以偽裝自己已經登入,甚至是將自己偽裝成任何用戶。
初級:未定義的變數
在 SQL Query 中,開發者假定 $usernmae
與 $password
會被 extract
函式建立,這是個非常危險的思想,後端開發者絕不應該「假定」任何用戶輸入都符合預期。
在 PHP 中,如果使用了未定義變數,它預設會被視為「空」,並且丟出警告(只有警告、不是錯誤)。在這個例子中,這兩個變數會被視為空字串。
理論上來說,這並不是什麼大問題。照常識上來說,資料庫中應該是不會存在 username
與 password
為空的資料(如果有,那表示資料表的設計存在一些邏輯上的問題)
初級:SQL Injection
我們知道,以單引號 ''
包裏的字串會以純文字呈現;雙引號 ""
包裏的字串會解析其變數值再呈現。
$name = 'Vincent';
echo 'Hello, $name'; // Hello, $name
echo "Hello, $name"; // Hello, Vincent
也就是說,以下程式中的 SQL Query 會被視為一個單純的字串,而其中的 $username
與 $password
是用戶可以控制的值。
<?php
$db = new DB();
$user = $db->query("SELECT * FROM users WHERE username=$username AND password=$password"); // query from DB
綜上所述,這段程式存在 SQL Injection 問題,因為用戶可以自由的輸入單引號、雙引號或註釋符號改變原本 SQL Query 的語義。