security-and-hardening

security-and-hardening

熱門

強化程式碼以抵禦漏洞。適用於處理使用者輸入、驗證、資料儲存或外部整合時。適用於建構任何接受不受信任資料、管理使用者工作階段或與第三方服務互動的功能。

7.7萬星標
0分支
更新於 2026/7/3
SKILL.md
readonlyread-only
name
security-and-hardening
description

強化程式碼以抵禦漏洞。適用於處理使用者輸入、驗證、資料儲存或外部整合時。適用於建構任何接受不受信任資料、管理使用者工作階段或與第三方服務互動的功能。

安全性與強化

概述

針對網頁應用程式的安全優先開發實務。將每個外部輸入視為敵意、每個機密視為神聖、每個授權檢查視為強制。安全性不是一個階段——它是每一行觸及使用者資料、驗證或外部系統的程式碼的約束。

使用時機

  • 建構任何接受使用者輸入的項目
  • 實作驗證或授權
  • 儲存或傳輸敏感資料
  • 與外部 API 或服務整合
  • 新增檔案上傳、Webhook 或回呼
  • 處理付款或 PII 資料

流程:先建立威脅模型

沒有威脅模型就加上控制措施只是猜測。在強化之前,花五分鐘像攻擊者一樣思考:

  1. 繪製信任邊界。 不受信任的資料從哪裡進入你的系統?HTTP 請求、表單欄位、檔案上傳、Webhook、第三方 API、訊息佇列,以及 LLM 輸出。每個邊界都是攻擊面。
  2. 列出資產。 什麼值得偷或破壞?憑證、PII、付款資料、管理員操作、資金轉移。
  3. 對每個邊界執行 STRIDE——快速檢視,而非儀式:
威脅 提問 典型緩解措施
Spoofing(偽造) 有人可以冒充使用者/服務嗎? 驗證、簽章驗證
Tampering(篡改) 資料在傳輸或儲存時可以被更改嗎? 完整性檢查、參數化查詢、HTTPS
Repudiation(否認) 某個動作事後可以被否認嗎? 安全事件的稽核日誌
Information disclosure(資訊揭露) 資料會外洩嗎? 加密、欄位允許清單、通用錯誤訊息
Denial of service(阻斷服務) 系統可以被壓垮嗎? 速率限制、輸入大小上限、逾時
Elevation of privilege(權限提升) 使用者可以獲得他們不該有的權限嗎? 授權檢查、最小權限原則
  1. 在使用案例旁邊寫下濫用案例。 對於每個功能,問「我該如何濫用這個?」——然後讓它成為你的第一個測試。

如果你無法說出某個功能的信任邊界,你還沒準備好保護它。這是 OWASP A04:不安全設計——大多數漏洞始於設計,而非程式碼。

三層邊界系統

務必執行(無例外)

  • 在系統邊界驗證所有外部輸入(API 路由、表單處理器)
  • 參數化所有資料庫查詢——絕不將使用者輸入串接到 SQL 中
  • 編碼輸出以防止 XSS(使用框架自動跳脫,不要繞過它)
  • 對所有外部通訊使用 HTTPS
  • 使用 bcrypt/scrypt/argon2 雜湊密碼(絕不儲存明文)
  • 設定安全標頭(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
  • 對工作階段使用 httpOnly、secure、sameSite Cookie
  • 在每次發佈前執行 npm audit(或同等工具)

先詢問(需人工核准)

  • 新增驗證流程或變更驗證邏輯
  • 儲存新類別的敏感資料(PII、付款資訊)
  • 新增外部服務整合
  • 變更 CORS 設定
  • 新增檔案上傳處理器
  • 修改速率限制或節流
  • 授予提升的權限或角色

絕不執行

  • 絕不將機密提交到版本控制(API 金鑰、密碼、Token)
  • 絕不記錄敏感資料(密碼、Token、完整信用卡號)
  • 絕不信任客戶端驗證作為安全邊界
  • 絕不為了方便而停用安全標頭
  • 絕不對使用者提供的資料使用 eval()innerHTML
  • 絕不將工作階段儲存在客戶端可存取的儲存空間(localStorage 中的驗證 Token)
  • 絕不向使用者暴露堆疊追蹤或內部錯誤細節

OWASP Top 10 預防模式

這些是預防模式,而非排名。如需 2021 年的排序,請參閱 references/security-checklist.md 中的快速參考表。

注入(SQL、NoSQL、OS 命令)

// 錯誤:透過字串串接的 SQL 注入
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// 正確:參數化查詢
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// 正確:使用參數化輸入的 ORM
const user = await prisma.user.findUnique({ where: { id: userId } });

驗證失效

// 密碼雜湊
import { hash, compare } from 'bcrypt';

const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);

// 工作階段管理
app.use(session({
  secret: process.env.SESSION_SECRET,  // 來自環境變數,而非程式碼
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // 無法透過 JavaScript 存取
    secure: true,       // 僅限 HTTPS
    sameSite: 'lax',    // CSRF 防護
    maxAge: 24 * 60 * 60 * 1000,  // 24 小時
  },
}));

跨站指令碼(XSS)

// 錯誤:將使用者輸入渲染為 HTML
element.innerHTML = userInput;

// 正確:使用框架自動跳脫(React 預設這樣做)
return <div>{userInput}</div>;

// 如果你必須渲染 HTML,先清理
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);

存取控制失效

// 務必檢查授權,而不只是驗證
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
  const task = await taskService.findById(req.params.id);

  // 檢查已驗證的使用者是否擁有此資源
  if (task.ownerId !== req.user.id) {
    return res.status(403).json({
      error: { code: 'FORBIDDEN', message: '無權修改此任務' }
    });
  }

  // 繼續更新
  const updated = await taskService.update(req.params.id, req.body);
  return res.json(updated);
});

安全設定錯誤

// 安全標頭(對 Express 使用 helmet)
import helmet from 'helmet';
app.use(helmet());

// 內容安全政策
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],  // 可能的話收緊
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'"],
  },
}));

// CORS — 限制為已知來源
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));

敏感資料暴露

// 絕不在 API 回應中回傳敏感欄位
function sanitizeUser(user: UserRecord): PublicUser {
  const { passwordHash, resetToken, ...publicFields } = user;
  return publicFields;
}

// 使用環境變數儲存機密
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY 未設定');

伺服器端請求偽造(SSRF)

每當伺服器擷取使用者影響的 URL 時——Webhook、「從 URL 匯入」、圖片代理、連結預覽——攻擊者可以將其指向內部服務(雲端中繼資料、localhost、私有 IP)。

// 錯誤:直接擷取使用者提供的任何內容
await fetch(req.body.webhookUrl);

// 正確:允許清單 scheme + host,若任何解析出的 IP 為私有則拒絕,禁止重新導向
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';

const ALLOWED_HOSTS = new Set(['hooks.example.com']);

async function assertSafeUrl(raw: string): Promise<URL> {
  const url = new URL(raw);
  if (url.protocol !== 'https:') throw new Error('僅允許 https');
  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('主機不允許');
  // 解析所有記錄;任何一個私有/保留位址都會使檢查失敗。
  const addrs = await lookup(url.hostname, { all: true });
  if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
    throw new Error('私有/保留 IP');
  }
  return url;
}

await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });

range() !== 'unicast' 檢查涵蓋了 IPv4 和 IPv6 的 loopback、link-local 169.254.169.254(雲端中繼資料,SSRF 的第一目標)、私有和唯一本地範圍。

注意——這仍然有 TOCTOU 差距。 fetch 在檢查後會再次解析 DNS,因此使用短 TTL 記錄的攻擊者可以在驗證和連線之間重新綁定到內部 IP。對於高風險表面,請解析一次並連線到固定的 IP,或在前端放置過濾代理(request-filtering-agent / ssrf-req-filter)。

輸入驗證模式

邊界上的 Schema 驗證

import { z } from 'zod';

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
});

// 在路由處理器中驗證
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: '無效的輸入',
        details: result.error.flatten(),
      },
    });
  }
  // result.data 現在已型別化且驗證過
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

檔案上傳安全

// 限制檔案類型和大小
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

function validateUpload(file: UploadedFile) {
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new ValidationError('不允許的檔案類型');
  }
  if (file.size > MAX_SIZE) {
    throw new ValidationError('檔案太大(最大 5MB)');
  }
  // 不要信任副檔名——如果關鍵,請檢查魔術位元組
}

處理 npm audit 結果

並非所有稽核發現都需要立即處理。使用此決策樹:

npm audit 回報漏洞
├── 嚴重性:critical 或 high
│   ├── 易受攻擊的程式碼在你的應用程式中是否可到達?
│   │   ├── 是 --> 立即修復(更新、修補或取代相依套件)
│   │   └── 否(僅開發相依、未使用的程式碼路徑)--> 盡快修復,但非阻礙
│   └── 是否有可用的修復?
│       ├── 是 --> 更新到已修補的版本
│       └── 否 --> 檢查替代方案,考慮取代相依套件,或加入允許清單並設定審查日期
├── 嚴重性:moderate
│   ├── 在生產環境中可到達?--> 在下一個發佈週期中修復
│   └── 僅開發?--> 方便時修復,在待辦事項中追蹤
└── 嚴重性:low
    └── 在定期相依套件更新時追蹤並修復

關鍵問題:

  • 易受攻擊的函式是否真的在你的程式碼路徑中被呼叫?
  • 相依套件是執行時期相依還是僅開發?
  • 根據你的部署環境,該漏洞是否可被利用(例如,僅客戶端應用程式中的伺服器端漏洞)?

當你延後修復時,請記錄原因並設定審查日期。

供應鏈衛生

npm audit 會捕捉已知的 CVE;它不會捕捉惡意或 typosquat 套件。此外:

  • 提交 lockfile 並在 CI 中使用 npm ci(而非 npm install)安裝——可重複的建置,無靜默版本漂移。
  • 在新增相依套件前先審查——維護狀況、下載次數,以及它們是否真正值得存在。每個相依套件都是攻擊面(OWASP A06:易受攻擊的元件LLM03:供應鏈)。
  • 對不熟悉的套件中的 postinstall 指令碼保持警覺——它們在安裝時會執行任意程式碼。
  • 留意 typosquat——cross-env vs crossenvreact-dom vs reactdom

速率限制

import rateLimit from 'express-rate-limit';

// 一般 API 速率限制
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分鐘
  max: 100,                   // 每個視窗 100 個請求
  standardHeaders: true,
  legacyHeaders: false,
}));

// 對驗證端點更嚴格的限制
app.use('/api/auth/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // 每 15 分鐘 10 次嘗試
}));

機密管理

.env 檔案:
  ├── .env.example  → 提交(包含佔位值的範本)
  ├── .env          → 不提交(包含真實機密)
  └── .env.local    → 不提交(本機覆蓋)

.gitignore 必須包含:
  .env
  .env.local
  .env.*.local
  *.pem
  *.key

提交前務必檢查:

# 檢查是否有意外暫存的機密
git diff --cached | grep -i "password\|secret\|api_key\|token"

如果機密曾被提交,請輪換它。 刪除該行或重寫歷史是不夠的——假設它一旦到達遠端就已遭洩露。先撤銷並重新發行金鑰,然後從歷史中清除它。

保護 AI / LLM 功能

如果你的應用程式呼叫 LLM——聊天機器人、摘要器、代理、RAG——它會繼承一個新的攻擊面。將其對應到 OWASP Top 10 for LLM Applications (2025)

  • 將所有模型輸出視為不受信任的輸入(LLM05:不當輸出處理)。 絕不將 LLM 輸出直接傳入 eval、SQL、shell、innerHTML 或檔案路徑。像對待原始使用者輸入一樣驗證和編碼它。
  • 假設提示可能被劫持(LLM01:提示注入)。 上下文視窗中的不受信任文字——使用者訊息、擷取的網頁、PDF——可能攜帶指令。系統提示不是安全邊界;在程式碼中強制執行權限,而非在提示中。
  • 將機密和其他使用者的資料排除在提示之外(LLM02 / LLM07)。 上下文中的任何內容都可能被回顯。不要將 API 金鑰、跨租戶資料或完整系統提示放在模型可以重複的地方。
  • 限制工具和代理的權限(LLM06:過度代理)。 將工具範圍限定到最小,對破壞性或不可逆的操作要求確認,並驗證每個工具引數。
  • 限制消耗(LLM10:無限制消耗)。 限制 Token、請求速率和迴圈/遞迴深度,以免精心設計的輸入導致成本飆升或系統掛起。
  • 隔離檢索資料(LLM08:向量和嵌入弱點)。 在 RAG 中,將向量資料庫視為信任邊界:按租戶分割嵌入,以便一個使用者無法檢索另一個使用者的資料,並在索引前驗證文件,以免有毒內容引導答案。
// 錯誤:信任模型輸出作為命令或標記
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql);                                   // 任意查詢執行
container.innerHTML = await llm.reply(userMessage);   // 儲存型 XSS,透過模型

// 正確:模型輸出是資料——防禦性解析,然後驗證,然後編碼
let intent;
try {
  intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
  throw new ValidationError('意外的模型輸出'); // JSON.parse 或 schema 失敗
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);

安全審查檢查清單

### 驗證
- [ ] 密碼使用 bcrypt/scrypt/argon2 雜湊(salt rounds ≥ 12)
- [ ] 工作階段 Token 為 httpOnly、secure、sameSite
- [ ] 登入有速率限制
- [ ] 密碼重設 Token 會過期

### 授權
- [ ] 每個端點檢查使用者權限
- [ ] 使用者只能存取自己的資源
- [ ] 管理員操作需要管理員角色驗證

### 輸入
- [ ] 所有使用者輸入在邊界處驗證
- [ ] SQL 查詢已參數化
- [ ] HTML 輸出已編碼/跳脫
- [ ] 伺服器端 URL 擷取已加入允許清單(無 SSRF 到內部服務)

### 資料
- [ ] 程式碼或版本控制中無機密
- [ ] 敏感欄位已從 API 回應中排除
- [ ] PII 在靜態時已加密(如適用)

### 基礎設施
- [ ] 安全標頭已設定(CSP、HSTS 等)
- [ ] CORS 限制為已知來源
- [ ] 相依套件已稽核漏洞
- [ ] 錯誤訊息不暴露內部資訊

### 供應鏈
- [ ] Lockfile 已提交;CI 使用 `npm ci` 安裝
- [ ] 新相依套件已審查(維護狀況、下載次數、postinstall 指令碼)

### AI / LLM(如使用)
- [ ] 模型輸出視為不受信任(無 eval/SQL/innerHTML/shell)
- [ ] 機密和其他使用者的資料已排除在提示之外
- [ ] 工具/代理權限已限定範圍;破壞性操作需要確認

另請參閱

如需詳細的安全檢查清單和提交前驗證步驟,請參閱 references/security-checklist.md

常見合理化藉口

合理化藉口 現實
「這是內部工具,安全性不重要」 內部工具也會被入侵。攻擊者會針對最弱的一環。
「我們之後再加入安全性」 安全性改造比一開始就建構難 10 倍。現在就加入。
「沒有人會試圖利用這個」 自動化掃描器會找到它。隱晦式安全不是安全。
「框架會處理安全性」 框架提供工具,而非保證。你仍然需要正確使用它們。
「這只是原型」 原型會變成正式產品。從第一天就養成安全習慣。
「威脅模型在這裡小題大作」 五分鐘的「我該如何攻擊這個?」可以防止後續沒有任何控制措施能修補的設計缺陷。
「這只是 LLM 輸出,只是文字」 那個「文字」可能是 SQL 陳述式、script 標籤或 shell 命令。像對待任何不受信任的輸入一樣對待它。

紅旗

  • 使用者輸入直接傳遞給資料庫查詢、shell 命令或 HTML 渲染
  • 原始碼或提交歷史中有機密
  • API 端點沒有驗證或授權檢查
  • 缺少 CORS 設定或使用萬用字元(*)來源
  • 驗證端點沒有速率限制
  • 向使用者暴露堆疊追蹤或內部錯誤
  • 相依套件有已知的重大漏洞
  • 伺服器擷取使用者提供的 URL 而沒有允許清單(SSRF)
  • LLM/模型輸出傳入查詢、DOM、shell 或 eval
  • 機密、PII 或完整系統提示放在 LLM 上下文視窗中

驗證

在實作安全相關程式碼後:

  • [ ] npm audit 顯示無 critical 或 high 漏洞
  • [ ] 原始碼或 git 歷史中無機密
  • [ ] 所有使用者輸入在系統邊界處驗證
  • [ ] 每個受保護的端點都檢查了驗證和授權
  • [ ] 回應中存在安全標頭(使用瀏覽器 DevTools 檢查)
  • [ ] 錯誤回應不暴露內部細節
  • [ ] 驗證端點上啟用了速率限制
  • [ ] 伺服器端 URL 擷取已根據允許清單驗證(無 SSRF)
  • [ ] LLM/模型輸出在使用前已驗證和編碼(如有 AI 功能)