iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
JavaScript

QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》系列 第 13 篇

Day13|innerHTML:我第一次在自己的網頁上被攻擊

  • 分享至 

  • xImage
  •  

今天的主題:innerHTML 與 textContent——以及我第一次在自己的網頁上被攻擊

前面幾天改畫面文字我一直用 textContent。今天要學它的兄弟 innerHTML,而這兩個的差別,對我這個 QA 來說比想像中重要。

差別:一個當文字,一個當程式碼

const str = "<b>粗體測試</b> 和 <span style='color:red'>紅色測試</span>";

test1.textContent = str;
test2.innerHTML = str;

同一段字串,兩個結果:

textContent → <b>粗體測試</b> 和 <span style='color:red'>紅色測試</span>
innerHTML   → 粗體測試 和 紅色測試          (真的變粗體、真的變紅色)

插入 day13.png:同一段字串在兩個屬性下的對比

屬性 把內容當成
textContent 純文字,原封不動顯示
innerHTML HTML 原始碼,會被解析並執行

拿來讓戰鬥訊息變好看

有了 innerHTML,戰鬥訊息就不只是一行乾巴巴的字:

logText.innerHTML =
  `<b>${hero.name}</b> 造成 <span style="color:red">${damage}</span> 點傷害!`;

名字變粗體、傷害數字變紅色。同樣的資料,看起來完全不同——這是遊戲第一次有了「視覺效果」而不只是文字。

插入 day13-3.png:戰鬥訊息「勇者 造成 17 點傷害!」,名字粗體、數字紅色

但「會被執行」這件事,是一個資安漏洞

假設之後要做「玩家自訂勇者名字」的功能(這本來就在我的計畫裡),玩家輸入什麼就顯示什麼。如果我用 innerHTML,而玩家把名字輸入成一段 HTML 呢?

我決定自己攻擊自己一次:

const evilName = `<img src=x onerror="alert('我控制了你的網頁')">`;
test1.textContent = evilName;   // 安全
test2.innerHTML = evilName;     // 危險

innerHTML 那行執行之後,畫面真的跳出了對話框:「我控制了你的網頁」。

插入 day13-2.png:alert 對話框跳出來,textContent 版原樣顯示那串程式碼

原理是:給圖片一個不存在的路徑(x),圖片載入必定失敗,失敗就觸發 onerror,而 onerror 裡寫的程式碼就被執行了。不需要 <script> 標籤也能執行程式,這是我最意外的地方。

這種攻擊叫 XSS(Cross-Site Scripting,跨站腳本攻擊)。今天跳出來的只是 alert,實際上可以做的事嚴重得多:偷走其他玩家的登入憑證、竄改畫面騙人輸入密碼、把惡意程式碼存進資料庫讓每個訪客都中招。

最可怕的細節:攻擊沒有留下痕跡

對照那兩行的結果:textContent 版原樣顯示了整串程式碼,看得見;而 innerHTML 版呢?

畫面上什麼都沒有。

那個 <img> 確實被插進去了,但它是一張破圖,沒有寬高、沒有替代文字,在畫面上完全隱形。也就是說:程式碼執行了,但頁面看起來一切正常。

今天是 alert 所以我看到了。如果攻擊者換成「偷走 cookie 傳到自己的伺服器」,畫面會完全正常,玩家永遠不會知道自己被攻擊過。

這又是「安靜失敗」家族的一員,只是這次安靜的那一方是攻擊者。從測試的角度看,這種問題比報錯的 bug 難抓太多了——因為沒有任何現象可以觀察。

這件事我在測試工作中見過另一個版本

看到這個結果,我馬上想起工作上聽過的另一種攻擊:有些密碼輸入框可以透過特定的輸入,繞過判斷直接登入。

追下去發現,這兩件事是同一個病根。

那種登入繞過叫 SQL injection。後端如果把使用者輸入直接拼進資料庫的查詢語句裡:

SELECT * FROM users WHERE name = '輸入的帳號' AND password = '輸入的密碼'

攻擊者只要在欄位裡輸入 ' OR '1'='1,拼起來就變成「密碼正確或者 1=1」——1=1 永遠成立,判斷就直接通過了。攻擊者沒有破解密碼,是讓系統忘了要檢查密碼。

對照一下:

XSS(今天遇到的) SQL injection
輸入被送進 HTML 資料庫查詢語句
系統誤把輸入當成 標籤和程式碼 查詢指令
結果 執行攻擊者的 JavaScript 繞過密碼檢查

共同的病根是同一句話:把使用者輸入的東西,當成程式碼執行了。

而正確解法的思路也一樣——明確區分「這是資料」和「這是指令」:

  • 網頁端:用 textContent(它永遠只當資料,從不執行任何東西)
  • 資料庫端:用參數化查詢(把值當參數傳進去,不要拼進語句裡)

寫了十三天 JavaScript,今天是第一次覺得「我的本業知識直接接上了」。以前測登入框我知道要試奇怪的輸入,但不太清楚背後為什麼會出事;今天親手做了一次,原理突然變得很具體。

我的判斷原則

// 內容完全由我自己寫死 → innerHTML 可以用
logText.innerHTML = `<b>${hero.name}</b> 造成 <span style="color:red">${damage}</span> 點傷害!`;

// 內容有任何一部分來自使用者 → 一律 textContent
playerNameText.textContent = playerInput;

這也解釋了為什麼前面十二天我一直只用 textContent——它天生安全,因為它從不執行任何東西。innerHTML 是有用的工具,但用它之前必須先確認一件事:這段字串裡,有沒有任何一個字是別人給的?

順帶一提,那段 XSS 測試碼我在推上 GitHub 之前已經註解掉了。它可以留著當紀錄,但不該留在會執行的狀態。

我用 innerHTML 做了一件「能動但不對」的事

學會 innerHTML 之後我馬上想到一個應用:讓擊敗小怪之後的「鞭屍!!」按鈕變成紅底白字,大一點,比較有警告感。

問題是,我當時手上只有 innerHTML 這一把工具。所以我這樣寫:

attackBtn.innerHTML =
  `<button style="background-color:#ff0000; color:#ffffff; padding:10px 20px; font-size:16px;">鞭屍!!</button>`;

視覺效果達成了,畫面上出現一顆紅色的按鈕。

插入 day13-4.png:紅底白字的鞭屍按鈕

但仔細看那張截圖,紅按鈕外面還有一圈細細的框。因為 attackBtn 本身就是一個 <button>,我剛剛做的事是把一個按鈕塞進另一個按鈕裡面:

<button id="attackBtn">
  <button style="...">鞭屍!!</button>
</button>

HTML 規格不允許按鈕嵌套按鈕。 而我實際測試按下去,訊息確實變成了「勇者請不要對小怪屍體動手!」——功能正常。

但它能動的原因是:點擊事件會往外層冒泡,所以外層那個真正掛了監聽器的按鈕還是被觸發了。也就是說,它能動是運氣,不是設計。瀏覽器對非法的 HTML 結構怎麼處理沒有保證,換一個瀏覽器、或改一點結構,行為就可能不一樣。

這種「測起來會過,但結構是錯的」情況,是我工作上最不喜歡的一類問題:因為測試通過了,所以沒有人會回頭看它,直到某天換了環境才爆掉。

而我後來查到,想改按鈕的顏色和大小根本不需要生一個新按鈕,直接改現有元素的樣式就好——那是明天要學的東西。所以這段程式碼我先留著,明天第一件事就是回來修掉它。

一個意外的插曲:錯誤訊息幫我擋了一次攻擊

第一次執行的時候,實驗 3 其實完全沒跑——alert 沒有跳出來,而且 test1 顯示的還是實驗 1 的字串。

原因是我在實驗 2 那行用了 logText,但那個檔案裡我還沒宣告它,於是噴了 ReferenceError: logText is not defined。Day 9 學過:執行期錯誤會從出錯那行開始,把後面的程式全部砍掉——所以實驗 3 連被讀到的機會都沒有。

我修好之後才看到那個 alert。也就是說,一個變數沒宣告的低級錯誤,意外地暫時擋住了一次 XSS。這當然不是什麼防禦機制,但它讓我更確定一件事:程式的執行順序真的很重要,而且錯誤訊息永遠值得認真讀完。

小結

今天搞懂的事:

  • textContent 把內容當純文字,innerHTML 會解析並執行 HTML
  • innerHTML 能讓畫面變好看(粗體、顏色),但它會執行你給它的東西
  • XSS:使用者輸入被當成 HTML 執行;<img src=x onerror="..."> 不需要 <script> 也能執行程式
  • XSS 攻擊可以完全不留視覺痕跡,這讓它特別難測
  • XSS 和 SQL injection 是同一個病根:把使用者輸入當成程式碼;解法都是明確區分資料與指令
  • 原則:字串完全自己寫死 → innerHTML 可用;有任何使用者輸入 → 一律 textContent
  • 用 innerHTML 生一顆按鈕塞進按鈕裡:測起來會過(事件冒泡救了它),但結構違法——能動不代表對

明天要讓畫面更有回饋:用 JS 改樣式,血量低的時候讓畫面變紅——順便回來修掉那顆嵌套的按鈕。


上一篇
Day12|從一片空白到打不死的小強:第一階段總結
下一篇
Day14|讓顏色說故事:白、灰、黑、紅的四階段戰鬥
系列文
QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言