18-5 資料隱碼(SQL Injection)

當你學會了資料庫與 ASP 的整合,一定很高興,而且急著把所有的資料都放到資料庫,以便進行更好的資料管理。在下面這個範例中,我們將使用者的密碼存放在資料庫之中,以對使用者的帳號和密碼進行有效的管理,此資料庫的內容如下(password.mdb):
userid passwd
CS3431 CS3431 
林政源 gavins 
陳江村 jtchen 
葉佳慧 beball 

根據此資料庫,我們就可以寫一個 ASP 網頁來進行帳號和密碼的認證:

Example(database/password01.asp):

其原始碼列出如下,以供讀者比較:

原始檔(database/password01.asp):(灰色區域按兩下即可拷貝)
<%@ language="jscript" %>
<% title="以資料庫內之資料進行密碼認證:基本篇" %>
<!--#include file="../head.inc"-->
<hr>

<% //利用ASP內建的Request物件取得表單欄位的「帳號」及「密碼」,'並判斷是否為空白。
x=Request("user")+"";
y=Request("passwd")+"";
if ((x=="undefined") && (y=="undefined")){ %>
	<% //顯示原有的表單欄位 %>
	<form method="post">
	請輸入帳號及密碼:
	<ul>
	<li>帳號:<Input name="user" value="CS3431"><br>
	<li>密碼:<Input type="password" name="passwd"><font color=white>密碼是:CS3431</font>
		<p><input type=submit><input type=reset>
	</ul>
	</form>
	(提示:按 ctrl-a 可以看到密碼喔!)
	<hr>
	<!--#include file="../foot.inc"-->
	<% Response.End();	 // 結束網頁 %>
<%}%>

<% //顯示查詢資料庫結果
//======建立ADO Connection,然後開啟Access資料庫
Conn = Server.CreateObject("ADODB.Connection");
database = "password.mdb";
Conn.ConnectionString = "DBQ=" + Server.MapPath(database) + ";Driver={Microsoft Access Driver (*.mdb)};Driverld=25;FIL=MS Access;";
Conn.Open();
//======從資料表中比較userid與passwd兩個欄位,看看是否和表單欄位user及passwd相同。
SQL = "select * from password where userid='" + Request("user") + "' and passwd='" + Request("passwd") + "'";
//======執行SQL指令,並將結果儲存於Recordset中
RS=Conn.Execute(SQL);
//======透過RecordSet集合取得欄位的內容
if (RS.EOF) {%>
	<p align=center>帳號或密碼錯誤!<br>SQL指令 = <u><font color=green><%=SQL%></font></u>
<%} else {%>
	<p align=center>帳號及密碼正確!<br>SQL指令 = <u><font color=green><%=SQL%></font></u>
<%}
//======關閉資料庫
RS.Close();
Conn.Close();
%>

<hr>
<!--#include file="../foot.inc"-->

看起來一切沒問題,但是如果你想「駭」(Hack!) 這個網站,事實上只要輸入下列資料就可以了:

(請趕快試試看!)這是為什麼呢?事實上這就是惡名昭彰的「資料隱碼」(SQL Injection)臭虫,簡單地說,就是將「帳號」和「密碼」填入具有單引號的特殊字串,造成伺服器端在接合這些欄位資料時,會意外地產生合格的 SQL 指令,造成密碼認證的成功。要特別注意的是,SQL Injection 的問題不限只發生在哪種特定平台或語言,只要是使用 SQL 指令存取資料庫內的資料,都有可能產生這個問題。

我們再來仔細看看上面這個範例,其中產生 SQL 指令的敘述如下:

SQL = "select * from password where userid='" + Request("user") + "' and passwd='" + Request("passwd") + "'"; 看起來邏輯完全正確,例如當輸入帳號和密碼分別是「林政源」和「gavins」時,所得到的 SQL 指令是:
SQL = "select * from password where userid='林政源' and passwd='gavins'";
所以可以從資料庫中查到一筆資料,代表帳號和密碼正確。但是當我們帳號和密碼分別是「xyz」和「' or 'a'='a」時,所得到的 SQL 指令是
SQL = "select * from password where userid='xyz' and passwd='' or 'a'='a'";
很不幸的,所產生的 SQL 指令也會執行成功(因為 'a'='a' 是一定成立的),因而從資料庫中抓出多筆資料,這種剪接手法彷彿是在 SQL 指令中「灌注」一些惡意的字串,所以稱為「SQL Injection」。

Hint
在 SQL 語法的條件式中,會先執行 and,再執行 or。

如何避免 SQL Injection 呢?最簡單的作法,就是在取用客戶端送進來的資料前,先刪除所有可能造成問題的特殊字元,這些字元包括單引號(')、雙引號(")、問號(?)、星號(*)、底線(_)、百分比(%)、Ampersand(&)等,這些特殊字元都不應該出現在使用者輸入的資料中。另外,刪除特殊字元的動作務必要在伺服器端進行,因為用戶端的 JavaScript 表單驗證的檢查是只能防君子,不能防小人,別人只要做一個有相同欄位的網頁,就一樣可以呼叫你的 ASP 程式碼來取用資料庫,進而避開原網頁的表單驗證功能。

若要刪除這些危險字元,可以使用 JavaScript 的字串的 replace() 方法,或是使用 VBScript 的 Replace 函數,例如:

Example(database/sqlInjection01.asp):

其原始碼列出如下:

原始檔(database/sqlInjection01.asp):(灰色區域按兩下即可拷貝)
<%@ language="jscript" %>
<% title="以資料庫內之資料進行密碼認證:如何避免 SQL Injection" %>
<!--#include file="../head.inc"-->
<hr>

<% //利用ASP內建的Request物件取得表單欄位的「帳號」及「密碼」,'並判斷是否為空白。
x=Request("user")+"";
y=Request("passwd")+"";
if ((x=="undefined") && (y=="undefined")){ %>
	<% //顯示原有的表單欄位 %>
	<form method="post">
	請輸入帳號及密碼:
	<ul>
	<li>帳號:<Input name="user" value="林政源"><br>
	<li>密碼:<Input type="password" name="passwd" value="gavins">
		<p><input type=submit><input type=reset>
	</ul>
	</form>
	(提示:按 F7 可以輸入 SQL Injection 所用之帳號和密碼!)
	<script>
	function fillForm() {
		if (event.keyCode==118) {
			document.forms[0].user.value="這是任意字串"
			document.forms[0].passwd.value="' or 'a'='a"
		}
	}
	</script>
	<script>document.onkeydown=fillForm;</script>
	<hr>
	<!--#include file="../foot.inc"-->
	<% Response.End();	 // 結束網頁 %>
<%}%>

<% //顯示查詢資料庫結果
//=======取得表單欄位內容
user = Request("user")+"";
passwd = Request("passwd")+"";
user = user.replace(/'/g, "");		//刪除單引號以避免 SQL Injection
passwd = passwd.replace(/'/g, "");	//刪除單引號以避免 SQL Injection
//=======建立ADO Connection,然後開啟Access資料庫
Conn = Server.CreateObject("ADODB.Connection");
database = "password.mdb";
Conn.ConnectionString = "DBQ=" + Server.MapPath(database) + ";Driver={Microsoft Access Driver (*.mdb)};Driverld=25;FIL=MS Access;";
Conn.Open();
//=======從資料表中比較userid與passwd兩個欄位,看看是否和表單欄位user及passwd相同。
SQL = "select * from password where userid='" + user + "' and passwd='" + passwd + "'";
RS=Conn.Execute(SQL);
if (RS.EOF) {%>
	<p align=center>帳號或密碼錯誤!<br>SQL指令 = "<u><font color=green><%=SQL%></font></u>"
<%} else {%>
	<p align=center>帳號及密碼正確!<br>SQL指令 = "<u><font color=green><%=SQL%></font></u>"
<%}
//======關閉資料庫
RS.Close();
Conn.Close();
%>

<hr>
<!--#include file="../foot.inc"-->

在上述原始碼中,因為 Request("userid") 和 Request("passwd") 的資料是無法修改的,所以在取代前要先存到另一個個變數。由此範例可以知道,只要刪除使用者輸入字串中的所有單引號,就可以避免 SQL Injection 的問題。

事實上,可以形成 SQL Injection 的惡意字串還不少,但大部分是針對微軟的 SQL Server 資料庫來進行破壞。若有興趣,讀者可自行參考下列參考資料:

如果你到 Google 打入「登入」,再對需要登入的網站進行 SQL Injection 的測試,就應該可以找到一些不設防的網站。請千萬不要作惡,若找到這些不設防的網站,將下列文字寄給此網站的維護者(也可將副本寄給我):

敬啟者:

我們研習張智星老師的「JavaScript程式設計與應用」,對網路上的網頁進行 SQL Injection 的測試,發覺您的登入網頁(網址是 http://xxx.xxx.xxx)並無法對抗 SQL Injection 的入侵,只要帳號任意設定、密碼設定為「' or 'a'='a」,即可登入。

這是一封善意的信,我們僅測試是否可以登入,並未對資料進行任何修改,請查照,謝謝。

(請寫出你的全名)

謝謝您的努力,這些網站的管理者會感謝你們的善心!
JScript 程式設計與應用:用於伺服器端的 ASP 環境