1995 年,JavaScript 剛被發明。
當時的工程師沒有想到,這個新技術會帶來一個嚴重的安全問題。
這個問題後來催生了一個重要的安全機制,影響網頁安全直到今天。
讓我們從那個時代說起。
1990 年代的網路:HTTP 與 HTML 的運作方式
在 1990 年代初期,網路非常單純。
基本上只有兩個角色:網頁伺服器和瀏覽器。
你在電腦上安裝一個瀏覽器,輸入一個網址(URL),瀏覽器就會向伺服器發送請求。
伺服器收到請求後,會回傳一段文字。
sequenceDiagram
participant 瀏覽器
participant 伺服器
瀏覽器->>伺服器: 請求網頁(輸入網址)
伺服器->>瀏覽器: 回傳 HTML 文字但這不是普通的文字。
伺服器回傳的是 HTML 文件。
HTML 看起來像這樣:
<html>
<body>
<h1>歡迎來到我的網站</h1>
<p>這是一段文字。</p>
</body>
</html>瀏覽器會解析這些 HTML 程式碼,然後把它渲染成你看到的網頁介面。
而網頁真正強大的地方在於:你可以用「超連結」把不同的資源串在一起。
點擊一個連結,瀏覽器就會跳到另一個頁面。
資訊因此被串連起來,瀏覽變得非常方便。
HTTP 無狀態的問題:網站無法記住使用者
大約在 1994 到 1995 年,Amazon 在網路上出現了。
越來越多真正的網路服務開始誕生:網路銀行、求職網站、網頁郵件、線上購物。
這時候,一個問題出現了。
想像一下這個情境:
你在購物網站上瀏覽商品,把一個東西加入購物車。
然後你點擊連結,到另一個頁面繼續逛。
當你逛完想結帳時,卻發現購物車是空的。
為什麼會這樣?
因為 HTTP 協定是「無狀態」的。
每一次瀏覽器向伺服器發送請求,對伺服器來說都是獨立的。
伺服器不會記得「這個人剛才做了什麼」。
你加入購物車的動作,伺服器處理完就忘了。
這對於需要「記住使用者狀態」的服務來說,是一個大問題。
不只是購物車,登入功能也有同樣的問題。
你在 A 頁面登入了,但換到 B 頁面時,伺服器不記得你登入過,又要求你重新登入。
Cookie 是什麼?1994 年解決無狀態問題的發明
為了解決這個問題,1994 年 6 月,Netscape 瀏覽器的開發者發明了 Cookie 機制。
這套機制包含兩個部分:
- Cookie 資料:伺服器傳送給瀏覽器的一小段文字資料
- Cookie 儲存區:瀏覽器用來存放這些資料的地方
那 Cookie 機制是怎麼運作的呢?
當你訪問網站時,伺服器可以傳一段資料給瀏覽器,瀏覽器會把這段資料存在 Cookie 儲存區裡。
這段資料通常包含:
- 身份識別資訊:例如 Session ID,讓伺服器知道你是誰
- 個人化設定:例如語言偏好、深色模式等使用者設定
之後你再訪問同一個網站時,瀏覽器會自動把這段資料傳回給伺服器。
這樣伺服器就能「認得」你了。
運作方式是這樣的:
- 你登入網站,伺服器驗證成功後,會回傳一個 Cookie 給瀏覽器
- 瀏覽器把這個 Cookie 存起來
- 之後每次你向這個網站發送請求,瀏覽器會自動帶上這個 Cookie
- 伺服器看到 Cookie,就知道「這是剛才登入的那個人」
sequenceDiagram
participant 瀏覽器
participant 伺服器
瀏覽器->>伺服器: 登入請求(帳號、密碼)
伺服器->>瀏覽器: 登入成功,回傳 Cookie
Note over 瀏覽器: 儲存 Cookie
瀏覽器->>伺服器: 請求其他頁面(自動帶上 Cookie)
伺服器->>瀏覽器: 認得你了,回傳頁面Cookie 解決了「記住使用者狀態」的問題。
購物車可以記住了,登入狀態也可以保持了。
Cookie 一開始是 Netscape 瀏覽器獨有的功能。
但因為它太實用了,其他瀏覽器也跟著實作,最後成為網頁的標準功能。
HTML 表單(Form):讓使用者輸入資料
同一時期,HTML 也增加了一個重要的功能:表單(form)。
表單讓使用者可以在網頁上輸入資料,然後提交給伺服器。
<form action="/login" method="POST">
<input type="text" name="username" placeholder="帳號">
<input type="password" name="password" placeholder="密碼">
<button type="submit">登入</button>
</form>有了表單,使用者可以:
- 輸入帳號密碼登入
- 輸入關鍵字搜尋
- 填寫訂單資訊
表單和 Cookie 搭配起來,網站就能提供完整的服務了。
使用者用表單輸入帳號密碼 → 伺服器驗證後回傳 Cookie → 之後的請求都帶上 Cookie → 使用者保持登入狀態。
靜態網頁的限制:為什麼需要動態互動?
到目前為止,網頁的互動模式是這樣的:
- 使用者點擊連結或提交表單
- 瀏覽器向伺服器發送請求
- 伺服器回傳新的 HTML
- 瀏覽器重新載入整個頁面
每一次互動,都要重新載入整個頁面。
這很慢,使用者體驗也不好。
工程師們開始想:有沒有辦法讓網頁在不重新載入的情況下,回應使用者的操作?
例如:
- 滑鼠移到按鈕上,按鈕變色
- 點擊一個選項,立刻顯示對應的內容
- 在表單輸入時,即時檢查格式是否正確
這些功能只靠 HTML 做不到。
為什麼?
因為 HTML 只是一種「標記語言」,它的作用是描述網頁的「結構」和「內容」。
例如,這裡有一個標題、那裡有一段文字、這邊有一張圖片。
HTML 可以告訴瀏覽器「網頁長什麼樣子」,但它沒辦法描述「當使用者做了某個動作,要怎麼回應」。
換句話說,HTML 是「靜態」的,它沒有「如果…就…」的邏輯能力。
你沒辦法用 HTML 寫出:「如果使用者點擊這個按鈕,就把那段文字變成紅色」。
要做到這件事,需要一種可以在瀏覽器裡執行的「程式語言」。
JavaScript 是什麼?1995 年讓網頁動起來的發明
1995 年,Netscape 2.0 的第一個測試版發布了。
發布說明中提到一個新功能:一種叫做「LiveScript」的腳本語言。
但很快地,在下一個測試版中,LiveScript 改名叫做 JavaScript。
發布說明是這樣寫的:
「Navigator 現在內建了一種叫做 JavaScript 的腳本語言。JavaScript 用
<script>標籤嵌入在 HTML 中,不需要編譯就可以執行。」
這裡的 Navigator 是 Netscape 瀏覽器的全名。
而這段話的重點是:JavaScript 是一種可以「在瀏覽器裡執行」的程式語言。
這是關鍵。
傳統的程式語言在執行前,需要先經過「編譯」,把程式碼轉換成電腦看得懂的格式,而且通常是在伺服器上執行。
使用者點擊按鈕 → 請求送到伺服器 → 伺服器處理 → 回傳新頁面。
但 JavaScript 不一樣。
它不需要編譯,寫在 HTML 裡,瀏覽器就可以直接執行。
使用者點擊按鈕 → 瀏覽器裡的 JavaScript 直接處理 → 立刻回應。
不需要等伺服器,不需要重新載入頁面。
有了 JavaScript,網頁可以:
- 回應使用者的點擊、滑鼠移動
- 即時修改網頁上的內容
- 讀取表單裡使用者輸入的資料
- 存取瀏覽器裡的 Cookie
網頁不再只是靜態的文件,而是可以「互動」的應用程式了。
這是一個革命性的改變。
和 Cookie 一樣,JavaScript 一開始也是 Netscape 獨有的功能,後來成為網頁的標準。
HTML Frame 是什麼?早期的網頁版面配置方式
同一時期,HTML 還有一個功能很流行:Frame(框架)。
Frame 是 HTML 的一種標籤,也是現在 <iframe> 的前身。
它可以把一個瀏覽器視窗分成多個區塊,每個區塊載入不同的網頁。
<frameset cols="200, *">
<frame src="navigation.html">
<frame src="content.html">
</frameset>這段程式碼會把視窗分成兩欄:
- 左邊載入
navigation.html(導覽列) - 右邊載入
content.html(主要內容)
graph LR
subgraph 瀏覽器視窗
A[導覽列<br>navigation.html]
B[主要內容<br>content.html]
endFrame 的好處是什麼?
還記得前面說的嗎?在沒有 JavaScript 的時代,每次互動都要重新載入整個頁面。
但有了 Frame,情況就不一樣了。
因為每個 Frame 都是獨立的區塊,有自己的網址,也可以有自己的名稱。
首先,用 <frameset> 定義版面配置,並幫每個 Frame 取名字:
<frameset cols="200, *">
<frame src="navigation.html" name="nav">
<frame src="content.html" name="content">
</frameset>這段程式碼做了幾件事:
cols="200, *":把畫面分成兩欄,左邊 200 像素,右邊填滿剩餘空間- 左邊的 Frame 載入
navigation.html,取名為nav - 右邊的 Frame 載入
content.html,取名為content
接著,在導覽列(navigation.html)裡面,連結可以用 target 屬性指定「要更新哪個 Frame」:
<a href="page2.html" target="content">第二頁</a>當使用者點擊「第二頁」這個連結時,瀏覽器不會更新整個頁面,而是只更新名為 content 的 Frame,載入 page2.html。
左邊的導覽列完全不會動。
使用者不用每次都等整個頁面重新載入,體驗更好,網頁載入也更快。
這在當時是很流行的網頁設計方式。
而且,Frame 裡的每個區塊可以載入不同網站的網頁。
例如:
<frameset cols="50%, 50%">
<frame src="https://my-site.com/page.html">
<frame src="https://other-site.com/page.html">
</frameset>左邊載入 my-site.com,右邊載入 other-site.com。
兩個完全不同的網站,可以同時顯示在同一個瀏覽器視窗裡。
這個功能在當時看起來很方便,但後來卻成為安全漏洞的根源。
早期的安全漏洞:JavaScript 可以跨網站存取資料
現在我們有了三個東西:
- Cookie:儲存使用者的登入狀態
- JavaScript:可以讀取網頁內容和 Cookie
- Frame:可以在同一個視窗裡載入多個不同的網站
把這三個東西加在一起,會發生什麼事?
讓我們看一個例子:
攻擊者建立一個惡意網站,網頁裡有兩個 Frame:
<frameset cols="50%, 50%">
<frame src="https://attacker.com/steal.html">
<frame src="https://bank.com/account">
</frameset>- 左邊的 Frame 載入攻擊者的網頁
- 右邊的 Frame 載入受害者的銀行網站
如果受害者之前登入過銀行,會發生什麼事?
還記得 Cookie 的運作方式嗎?
當你登入銀行網站後,瀏覽器會把銀行給的 Cookie 存起來。
之後只要瀏覽器向 bank.com 發送請求,就會自動帶上這個 Cookie。
現在,右邊的 Frame 載入 bank.com/account。
對瀏覽器來說,這就是一個向 bank.com 發送的請求,所以它會自動帶上之前存的 Cookie。
銀行伺服器收到 Cookie,確認身份,回傳受害者的帳戶資訊。
結果就是:右邊的 Frame 會顯示受害者的銀行帳戶資訊,包括餘額、交易紀錄等等。
現在,攻擊者的網頁裡有這段 JavaScript:
function stealData() {
// 存取另一個 Frame 的內容
var bankFrame = top.frames[1];
var bankDocument = bankFrame.document;
// 讀取銀行網頁上的帳戶餘額
var balance = bankDocument.getElementById('balance').innerText;
// 讀取銀行網站的 Cookie
var cookie = bankDocument.cookie;
// 把偷到的資料傳送給攻擊者
sendToAttacker(balance, cookie);
}是的,在 1995 年的 Netscape 瀏覽器裡,這段程式碼是可以執行的。
攻擊者的 JavaScript 可以直接存取另一個 Frame 裡的銀行網頁,讀取帳戶餘額、交易紀錄、Cookie。
只要受害者打開這個惡意網頁,他的銀行資料就被偷走了。
從今天的角度來看,這太瘋狂了。
但當時的工程師才剛發明 JavaScript,他們還沒意識到這個問題。
另一個安全漏洞:JavaScript 可以讀取本機檔案
除了跨網站存取的問題,還有另一個漏洞。
瀏覽器不只可以載入網路上的網頁,也可以載入本機電腦的檔案。
你可以在網址列輸入 file:///C:/ 來瀏覽本機的檔案系統。
攻擊者發現,他們可以用 Frame 載入本機的資料夾。
什麼意思?
瀏覽器不只可以載入網路上的網頁,也可以瀏覽本機電腦的檔案系統。
你可以在網址列輸入 file:///C:/,瀏覽器就會顯示 C 槽的檔案列表,就像檔案總管一樣。
這個功能本身沒有問題。
問題在於:當時的瀏覽器允許 attacker.com 的網頁,用 Frame 載入你電腦裡的 file:///C:/,然後用 JavaScript 讀取裡面的內容。
攻擊者利用這個特性,在惡意網頁裡用 Frame 載入受害者的本機資料夾:
<frameset>
<frame src="file:///C:/">
</frameset>當受害者打開這個網頁,右邊的 Frame 會顯示他電腦裡 C 槽的所有檔案和資料夾。
然後,攻擊者用 JavaScript 讀取這些檔案名稱:
// 存取 Frame 裡的內容
var fileFrame = top.frames[0];
var fileList = fileFrame.document.links;
// 讀取所有檔案名稱
for (var i = 0; i < fileList.length; i++) {
console.log(fileList[i].href);
// 輸出:file:///C:/Windows
// 輸出:file:///C:/Users
// 輸出:file:///C:/秘密文件.doc
// ...
}攻擊者可以知道你電腦裡有什麼檔案,包括檔案名稱和資料夾結構。
這可能是史上第一個被發現的 JavaScript 安全漏洞。
同源政策(Same Origin Policy)的誕生:瀏覽器如何修復漏洞
Netscape 的工程師很快意識到這些問題的嚴重性。
1996 年,他們在 Netscape 2.02 版本中修復了這些漏洞。
這是一個瀏覽器端的修正。
修復的方式是:在瀏覽器裡加入一個安全機制,限制 JavaScript 只能存取「同源」的內容。
什麼是「同源」?
Netscape 3.0 的發布說明裡這樣寫:
「2.0.2 版本之後,會自動阻止來自某個伺服器的腳本,存取來自另一個伺服器的文件內容。」
這裡說的「來自某個伺服器的腳本」,指的是「A 網站回傳的網頁中所包含的 JavaScript,在瀏覽器裡執行時」。
換句話說,瀏覽器會檢查:
- 這段 JavaScript 是從哪個網站來的?
- 它想要存取的內容是從哪個網站來的?
如果兩者不是同一個來源,瀏覽器就會阻止存取。
但瀏覽器是怎麼知道 JavaScript 是從哪個網站來的?
JavaScript 程式碼本身不會標註「我是從哪個網站來的」。
瀏覽器的判斷方式是:看這段 JavaScript 是在哪個網頁裡執行的。
舉例來說:
- 你打開
https://attacker.com/evil.html - 這個網頁裡面有一段 JavaScript
- 不管這段 JavaScript 的程式碼寫了什麼,瀏覽器都會認定它的「來源」是
attacker.com
換句話說,JavaScript 的來源 = 它所在網頁的網址。
所以當 attacker.com 網頁裡的 JavaScript 嘗試存取 bank.com 的 Frame 內容時,瀏覽器會這樣判斷:
- JavaScript 的來源:
attacker.com - 要存取的內容來源:
bank.com - 兩者不同 → 阻止存取
舉例來說:
attacker.com的 JavaScript 不能存取bank.com的內容(不同網站)bank.com的 JavaScript 不能存取file:///C:/的內容(網站 vs 本機檔案)bank.com的 JavaScript 只能存取bank.com的內容(同一個來源)
這就是「同源政策」(Same Origin Policy)的誕生。
同源的定義:協定、網域、連接埠
那麼,什麼情況下兩個網頁是「同源」的呢?
同源的定義是:協定(Protocol)、網域(Domain)、連接埠(Port)都要相同。
舉個例子:
只要有一個不同,瀏覽器就會認為是「不同源」,JavaScript 就無法存取另一個網頁的資料。
同源政策保護了什麼?Cookie、DOM、AJAX
同源政策主要保護以下這些東西:
Cookie 和 Session
如果沒有同源政策,惡意網站可以直接讀取你在銀行網站的 Cookie,然後冒充你的身份。
DOM 存取
如果沒有同源政策,惡意網站可以用 iframe 嵌入你的 Gmail,然後用 JavaScript 讀取你的郵件內容。
AJAX 請求
如果沒有同源政策,惡意網站可以用你的瀏覽器向銀行網站發送請求,進行轉帳操作。
同源政策與現代網頁安全:XSS、CSRF、Clickjacking
同源政策從 1995 年誕生至今,已經超過 30 年。
它依然是瀏覽器最重要的安全機制之一。
現代常見的安全攻擊,像是 XSS(跨站腳本攻擊)、CSRF(跨站請求偽造)、Clickjacking(點擊劫持),都和同源政策有關:
- 有些攻擊是試圖繞過同源政策
- 有些攻擊是利用同源政策保護不到的地方
了解同源政策,是理解網頁安全的基礎。
小結
讓我們回顧一下這篇文章的重點:
- 早期的網路世界:只有伺服器和瀏覽器,透過 HTTP 傳輸 HTML 文件
- Cookie 的誕生:1994 年,為了解決「記住使用者狀態」的問題而發明
- JavaScript 的誕生:1995 年,讓網頁變得動態,但也帶來安全問題
- 第一個 JavaScript 漏洞:一個網站的 JavaScript 可以存取另一個網站的資料
- 同源政策的誕生:1996 年,Netscape 修復漏洞,規定「一個來源的腳本不能存取另一個來源的資料」
- 同源的定義:協定、網域、連接埠都要相同
- 同源政策的重要性:它是現代網頁安全的基礎,XSS、CSRF 等攻擊都和它有關
同源政策從 1995 年誕生至今,已經超過 30 年。
它依然是瀏覽器最重要的安全機制之一。
了解這段歷史,可以幫助你更深入理解為什麼現代網頁安全是這樣設計的。