Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

本文為「從零開始的 Web 資安」系列第 10 篇

什麼是同源政策(Same Origin Policy)?從 1995 年的 Netscape 瀏覽器說起

最後更新:2026年1月29日架構

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,讓伺服器知道你是誰
  • 個人化設定:例如語言偏好、深色模式等使用者設定

之後你再訪問同一個網站時,瀏覽器會自動把這段資料傳回給伺服器。

這樣伺服器就能「認得」你了。

運作方式是這樣的:

  1. 你登入網站,伺服器驗證成功後,會回傳一個 Cookie 給瀏覽器
  2. 瀏覽器把這個 Cookie 存起來
  3. 之後每次你向這個網站發送請求,瀏覽器會自動帶上這個 Cookie
  4. 伺服器看到 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 → 使用者保持登入狀態。

靜態網頁的限制:為什麼需要動態互動?

到目前為止,網頁的互動模式是這樣的:

  1. 使用者點擊連結或提交表單
  2. 瀏覽器向伺服器發送請求
  3. 伺服器回傳新的 HTML
  4. 瀏覽器重新載入整個頁面

每一次互動,都要重新載入整個頁面。

這很慢,使用者體驗也不好。

工程師們開始想:有沒有辦法讓網頁在不重新載入的情況下,回應使用者的操作?

例如:

  • 滑鼠移到按鈕上,按鈕變色
  • 點擊一個選項,立刻顯示對應的內容
  • 在表單輸入時,即時檢查格式是否正確

這些功能只靠 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]
    end

Frame 的好處是什麼?

還記得前面說的嗎?在沒有 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 可以跨網站存取資料

現在我們有了三個東西:

  1. Cookie:儲存使用者的登入狀態
  2. JavaScript:可以讀取網頁內容和 Cookie
  3. 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 是在哪個網頁裡執行的。

舉例來說:

  1. 你打開 https://attacker.com/evil.html
  2. 這個網頁裡面有一段 JavaScript
  3. 不管這段 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)都要相同。

舉個例子:

URL是否與 https://example.com/page 同源原因
https://example.com/other✅ 同源協定、網域、連接埠都相同
http://example.com/page❌ 不同源協定不同(http vs https)
https://api.example.com/page❌ 不同源網域不同(子網域也算不同)
https://example.com:8080/page❌ 不同源連接埠不同
是否與 https://example.com/page 同源✅ 同源
原因協定、網域、連接埠都相同
是否與 https://example.com/page 同源❌ 不同源
原因協定不同(http vs https)
是否與 https://example.com/page 同源❌ 不同源
原因網域不同(子網域也算不同)
是否與 https://example.com/page 同源❌ 不同源
原因連接埠不同

只要有一個不同,瀏覽器就會認為是「不同源」,JavaScript 就無法存取另一個網頁的資料。

同源政策保護了什麼?Cookie、DOM、AJAX

同源政策主要保護以下這些東西:

Cookie 和 Session

如果沒有同源政策,惡意網站可以直接讀取你在銀行網站的 Cookie,然後冒充你的身份。

DOM 存取

如果沒有同源政策,惡意網站可以用 iframe 嵌入你的 Gmail,然後用 JavaScript 讀取你的郵件內容。

AJAX 請求

如果沒有同源政策,惡意網站可以用你的瀏覽器向銀行網站發送請求,進行轉帳操作。

同源政策與現代網頁安全:XSS、CSRF、Clickjacking

同源政策從 1995 年誕生至今,已經超過 30 年。

它依然是瀏覽器最重要的安全機制之一。

現代常見的安全攻擊,像是 XSS(跨站腳本攻擊)、CSRF(跨站請求偽造)、Clickjacking(點擊劫持),都和同源政策有關:

  • 有些攻擊是試圖繞過同源政策
  • 有些攻擊是利用同源政策保護不到的地方

了解同源政策,是理解網頁安全的基礎。

小結

讓我們回顧一下這篇文章的重點:

  1. 早期的網路世界:只有伺服器和瀏覽器,透過 HTTP 傳輸 HTML 文件
  2. Cookie 的誕生:1994 年,為了解決「記住使用者狀態」的問題而發明
  3. JavaScript 的誕生:1995 年,讓網頁變得動態,但也帶來安全問題
  4. 第一個 JavaScript 漏洞:一個網站的 JavaScript 可以存取另一個網站的資料
  5. 同源政策的誕生:1996 年,Netscape 修復漏洞,規定「一個來源的腳本不能存取另一個來源的資料」
  6. 同源的定義:協定、網域、連接埠都要相同
  7. 同源政策的重要性:它是現代網頁安全的基礎,XSS、CSRF 等攻擊都和它有關

同源政策從 1995 年誕生至今,已經超過 30 年。

它依然是瀏覽器最重要的安全機制之一。

了解這段歷史,可以幫助你更深入理解為什麼現代網頁安全是這樣設計的。

上一篇新手指南:深入了解 Content-Security-Policy (CSP) 與網站安全
下一篇跨來源資源共享(CORS)完整指南:打破瀏覽器的安全邊界
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

架構

目錄

  • 1990 年代的網路:HTTP 與 HTML 的運作方式
  • HTTP 無狀態的問題:網站無法記住使用者
  • Cookie 是什麼?1994 年解決無狀態問題的發明
  • HTML 表單(Form):讓使用者輸入資料
  • 靜態網頁的限制:為什麼需要動態互動?
  • JavaScript 是什麼?1995 年讓網頁動起來的發明
  • HTML Frame 是什麼?早期的網頁版面配置方式
  • 早期的安全漏洞:JavaScript 可以跨網站存取資料
  • 另一個安全漏洞:JavaScript 可以讀取本機檔案
  • 同源政策(Same Origin Policy)的誕生:瀏覽器如何修復漏洞
  • 同源的定義:協定、網域、連接埠
  • 同源政策保護了什麼?Cookie、DOM、AJAX
  • Cookie 和 Session
  • DOM 存取
  • AJAX 請求
  • 同源政策與現代網頁安全:XSS、CSRF、Clickjacking
  • 小結