iT邦幫忙

2025 iThome 鐵人賽

0
Modern Web

前端工程師的 Modern Web 實踐之道系列 第 16

前端安全實踐:XSS、CSRF 到 CSP 的現代化防護策略

  • 分享至 

  • xImage
  •  

系列文章: 前端工程師的 Modern Web 實踐之道 - Day 17
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆

🎯 今日目標

在前一篇文章中,我們深入探討了程式碼品質保證的工程化實踐。今天我們將聚焦於一個常被忽視但極其重要的主題——前端安全。這個主題將幫助你建立完整的 Web 應用安全防護體系,避免常見的安全漏洞,保護使用者資料和隱私。

為什麼要關注前端安全?

  • 真實威脅: 根據 OWASP 2021 報告,XSS 和 CSRF 仍是最常見的 Web 攻擊手法
  • 資料外洩風險: 不當的前端實作可能導致使用者敏感資料外洩
  • 品牌信譽損害: 安全事件會嚴重影響使用者信任和企業聲譽
  • 法律合規要求: GDPR、個資法等法規對資料安全有嚴格要求
  • 技術專業性體現: 安全意識是區分初級和高級工程師的重要指標

🔍 深度分析:前端安全的技術本質

問題背景與現狀

許多前端工程師認為「安全是後端的責任」,這是一個危險的誤解。事實上:

  1. 前端是第一道防線: 使用者直接接觸的是前端介面,前端安全直接影響使用者體驗
  2. 客戶端攻擊日益增多: 隨著前端功能複雜化,客戶端成為攻擊的主要目標
  3. 現代框架並非萬能: React、Vue 等框架提供了一定防護,但並非絕對安全
  4. 供應鏈攻擊風險: npm 套件的安全漏洞可能影響整個應用

常見的前端安全威脅

1. XSS (Cross-Site Scripting) - 跨站腳本攻擊

XSS 是最常見的前端安全漏洞,攻擊者通過注入惡意腳本來竊取使用者資料或執行未授權操作。

三種 XSS 類型:

  • Stored XSS (儲存型): 惡意腳本儲存在伺服器資料庫中
  • Reflected XSS (反射型): 惡意腳本通過 URL 參數注入
  • DOM-based XSS (DOM 型): 完全在客戶端執行,不經過伺服器

2. CSRF (Cross-Site Request Forgery) - 跨站請求偽造

CSRF 攻擊利用使用者的已登入狀態,在使用者不知情的情況下發送惡意請求。

3. 資料外洩與敏感資訊暴露

  • 在客戶端程式碼中硬編碼 API 金鑰、密碼
  • 通過瀏覽器 DevTools 可以查看的敏感資料
  • 未加密的本地儲存資料

4. 不安全的第三方依賴

  • 使用有已知漏洞的 npm 套件
  • 載入不受信任的第三方腳本
  • CDN 資源被篡改

💻 實戰演練:建立完整的安全防護體系

1. XSS 防護實踐

基礎防護:輸入驗證與輸出編碼

/**
 * HTML 特殊字元編碼 - 防止 XSS 注入
 * @param {string} str - 需要編碼的字串
 * @returns {string} 編碼後的安全字串
 */
function escapeHtml(str) {
  const htmlEscapeMap = {
    '&': '&',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  };

  return String(str).replace(/[&<>"'/]/g, (char) => htmlEscapeMap[char]);
}

/**
 * 安全的 DOM 更新函式
 * @param {HTMLElement} element - 目標元素
 * @param {string} content - 要插入的內容
 */
function safeSetContent(element, content) {
  // 使用 textContent 而非 innerHTML
  element.textContent = content;
}

/**
 * 如果必須使用 HTML,請使用 DOMPurify 清理
 */
import DOMPurify from 'dompurify';

function setSafeHTML(element, html) {
  const cleanHTML = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'title', 'target']
  });
  element.innerHTML = cleanHTML;
}

// 實際使用範例
const userInput = '<script>alert("XSS")</script><b>Hello</b>';
const safeContent = escapeHtml(userInput);
console.log(safeContent); // &lt;script&gt;alert(&quot;XSS&quot;)&lt;&#x2F;script&gt;&lt;b&gt;Hello&lt;&#x2F;b&gt;

// 使用 DOMPurify 清理 HTML
const container = document.getElementById('content');
setSafeHTML(container, userInput); // 只保留 <b>Hello</b>,移除 script 標籤

React 框架中的 XSS 防護

import React, { useState } from 'react';
import DOMPurify from 'isomorphic-dompurify';

/**
 * 安全的使用者評論組件
 */
interface CommentProps {
  author: string;
  content: string;
  allowHTML?: boolean;
}

const SafeComment: React.FC<CommentProps> = ({
  author,
  content,
  allowHTML = false
}) => {
  // React 預設會轉義文字內容,這是安全的
  if (!allowHTML) {
    return (
      <div className="comment">
        <strong>{author}</strong>
        <p>{content}</p>
      </div>
    );
  }

  // 如果必須支援 HTML,使用 DOMPurify 清理
  const sanitizedContent = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'title', 'target'],
    ALLOW_DATA_ATTR: false
  });

  return (
    <div className="comment">
      <strong>{author}</strong>
      <div
        dangerouslySetInnerHTML={{ __html: sanitizedContent }}
      />
    </div>
  );
};

/**
 * 處理使用者輸入的表單組件
 */
const CommentForm: React.FC = () => {
  const [comment, setComment] = useState('');
  const [error, setError] = useState('');

  /**
   * 輸入驗證 - 第一道防線
   */
  const validateInput = (input: string): boolean => {
    // 檢查長度
    if (input.length > 1000) {
      setError('評論內容不能超過 1000 字');
      return false;
    }

    // 檢查危險模式
    const dangerousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+=/i, // 事件處理器如 onclick=
      /<iframe/i
    ];

    for (const pattern of dangerousPatterns) {
      if (pattern.test(input)) {
        setError('評論包含不允許的內容');
        return false;
      }
    }

    setError('');
    return true;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!validateInput(comment)) {
      return;
    }

    try {
      // 送到後端前再次清理
      const sanitized = DOMPurify.sanitize(comment);

      await fetch('/api/comments', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ content: sanitized })
      });

      setComment('');
    } catch (err) {
      console.error('提交失敗:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="請輸入評論..."
        maxLength={1000}
      />
      {error && <p className="error">{error}</p>}
      <button type="submit">送出評論</button>
    </form>
  );
};

export { SafeComment, CommentForm };

2. CSRF 防護實踐

Token 驗證機制

/**
 * CSRF Token 管理器
 */
class CSRFTokenManager {
  constructor() {
    this.tokenKey = 'csrf_token';
    this.tokenHeaderName = 'X-CSRF-Token';
  }

  /**
   * 從 Cookie 或 Meta 標籤獲取 CSRF Token
   */
  getToken() {
    // 方法 1: 從 Meta 標籤讀取 (由後端渲染時注入)
    const metaToken = document.querySelector('meta[name="csrf-token"]');
    if (metaToken) {
      return metaToken.getAttribute('content');
    }

    // 方法 2: 從 Cookie 讀取
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const [name, value] = cookie.trim().split('=');
      if (name === this.tokenKey) {
        return decodeURIComponent(value);
      }
    }

    return null;
  }

  /**
   * 為 fetch 請求添加 CSRF Token
   */
  async secureFetch(url, options = {}) {
    const token = this.getToken();

    if (!token) {
      throw new Error('CSRF token not found');
    }

    const secureOptions = {
      ...options,
      headers: {
        ...options.headers,
        [this.tokenHeaderName]: token,
      },
      credentials: 'same-origin', // 確保發送 Cookie
    };

    return fetch(url, secureOptions);
  }
}

// 實際使用
const csrfManager = new CSRFTokenManager();

// 安全的 POST 請求
async function submitForm(data) {
  try {
    const response = await csrfManager.secureFetch('/api/user/update', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('請求失敗:', error);
    throw error;
  }
}

Axios 攔截器整合 CSRF 防護

import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

/**
 * 創建安全的 Axios 實例
 */
function createSecureAxios(): AxiosInstance {
  const instance = axios.create({
    baseURL: '/api',
    timeout: 10000,
    withCredentials: true, // 發送 Cookie
  });

  // 請求攔截器 - 添加 CSRF Token
  instance.interceptors.request.use(
    (config) => {
      // 只對非 GET 請求添加 CSRF Token
      if (config.method && !['get', 'head', 'options'].includes(config.method.toLowerCase())) {
        const token = getCSRFToken();
        if (token) {
          config.headers['X-CSRF-Token'] = token;
        }
      }

      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );

  // 回應攔截器 - 處理 CSRF 錯誤
  instance.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 403) {
        const errorMessage = error.response.data?.message || '';
        if (errorMessage.includes('CSRF')) {
          // CSRF token 無效,可能需要重新整理頁面
          console.error('CSRF token 驗證失敗,請重新整理頁面');
          // 可以觸發重新載入或導向登入頁
        }
      }
      return Promise.reject(error);
    }
  );

  return instance;
}

function getCSRFToken(): string | null {
  const metaToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
  return metaToken?.content || null;
}

// 使用範例
const apiClient = createSecureAxios();

export async function updateUserProfile(data: any) {
  try {
    const response = await apiClient.post('/user/profile', data);
    return response.data;
  } catch (error) {
    console.error('更新失敗:', error);
    throw error;
  }
}

SameSite Cookie 設定

/**
 * 伺服器端 Cookie 設定範例 (Node.js/Express)
 * 這是後端設定,但前端工程師需要了解
 */

// Express 中設定安全的 Session Cookie
app.use(session({
  secret: process.env.SESSION_SECRET,
  name: 'sessionId',
  cookie: {
    httpOnly: true,      // 防止 JavaScript 存取 Cookie
    secure: true,        // 只在 HTTPS 連線中傳送
    sameSite: 'strict',  // 嚴格的同站策略,防止 CSRF
    maxAge: 3600000,     // 1 小時過期
    domain: '.example.com', // 限制 Cookie 作用域
  },
  resave: false,
  saveUninitialized: false
}));

// 設定 CSRF Token Cookie
app.use((req, res, next) => {
  res.cookie('XSRF-TOKEN', req.csrfToken(), {
    httpOnly: false,     // 允許 JavaScript 讀取 (用於發送請求)
    secure: true,
    sameSite: 'strict',
    maxAge: 3600000
  });
  next();
});

3. Content Security Policy (CSP) 實踐

CSP 是現代瀏覽器提供的強大安全機制,可以有效防止 XSS 攻擊。

CSP 設定策略

/**
 * CSP 設定範例 - 伺服器端 Header 設定
 */

// Express 中設定 CSP
const helmet = require('helmet');

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],  // 預設只允許同源資源

      scriptSrc: [
        "'self'",                           // 同源腳本
        "'nonce-{RANDOM_NONCE}'",          // 使用 nonce 的行內腳本
        "https://cdn.jsdelivr.net",        // 信任的 CDN
        "https://www.google-analytics.com" // 分析工具
      ],

      styleSrc: [
        "'self'",
        "'unsafe-inline'",                 // 允許行內樣式 (考慮使用 nonce 替代)
        "https://fonts.googleapis.com"
      ],

      imgSrc: [
        "'self'",
        "data:",                           // 允許 data: URI
        "https:",                          // 允許 HTTPS 圖片
      ],

      fontSrc: [
        "'self'",
        "https://fonts.gstatic.com"
      ],

      connectSrc: [
        "'self'",
        "https://api.example.com",         // API 伺服器
        "wss://realtime.example.com"       // WebSocket 連線
      ],

      frameSrc: ["'none'"],                // 不允許嵌入 iframe

      objectSrc: ["'none'"],               // 不允許 <object> 元素

      baseUri: ["'self'"],                 // 限制 <base> 標籤

      formAction: ["'self'"],              // 限制表單提交目標

      upgradeInsecureRequests: [],         // 自動升級 HTTP 到 HTTPS
    },
  })
);

使用 Nonce 的安全行內腳本

<!-- HTML 模板中使用 nonce -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- CSP Meta 標籤 (也可以用 HTTP Header) -->
  <meta http-equiv="Content-Security-Policy"
        content="script-src 'self' 'nonce-{RANDOM_NONCE}'">
</head>
<body>
  <!-- 安全的行內腳本,使用 nonce 屬性 -->
  <script nonce="{RANDOM_NONCE}">
    // 這個腳本會被執行,因為 nonce 匹配
    console.log('This is a safe inline script');
  </script>

  <script>
    // 這個腳本會被 CSP 阻止,因為沒有 nonce
    console.log('This will be blocked');
  </script>

  <!-- 外部腳本 -->
  <script src="/js/app.js"></script>
</body>
</html>
/**
 * 後端生成和注入 nonce 的範例
 */
const crypto = require('crypto');

app.use((req, res, next) => {
  // 為每個請求生成唯一的 nonce
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.get('/', (req, res) => {
  // 在模板中使用 nonce
  res.render('index', {
    nonce: res.locals.nonce
  });
});

CSP 違規監控

/**
 * CSP 違規報告處理
 */

// 設定 CSP 報告端點
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      // ... 其他 CSP 設定
      reportUri: '/api/csp-violation-report',
    },
  })
);

// 處理 CSP 違規報告
app.post('/api/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const violation = req.body['csp-report'];

  // 記錄違規資訊
  console.error('CSP Violation:', {
    documentUri: violation['document-uri'],
    violatedDirective: violation['violated-directive'],
    blockedUri: violation['blocked-uri'],
    sourceFile: violation['source-file'],
    lineNumber: violation['line-number'],
    columnNumber: violation['column-number'],
  });

  // 發送到日誌系統或監控平台
  logToMonitoringService({
    type: 'csp_violation',
    ...violation,
    timestamp: new Date().toISOString(),
  });

  res.status(204).send();
});

4. 安全的資料儲存

/**
 * 安全的本地儲存管理器
 */
class SecureStorage {
  private readonly encryptionKey: string;
  private readonly storagePrefix: string = 'secure_';

  constructor(encryptionKey: string) {
    this.encryptionKey = encryptionKey;
  }

  /**
   * 簡單的 XOR 加密 (實際專案應使用 Web Crypto API)
   */
  private encrypt(data: string): string {
    let result = '';
    for (let i = 0; i < data.length; i++) {
      result += String.fromCharCode(
        data.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)
      );
    }
    return btoa(result); // Base64 編碼
  }

  private decrypt(encryptedData: string): string {
    const data = atob(encryptedData); // Base64 解碼
    return this.encrypt(data); // XOR 加密是對稱的
  }

  /**
   * 安全儲存資料
   */
  setItem(key: string, value: any): void {
    try {
      const jsonData = JSON.stringify(value);
      const encrypted = this.encrypt(jsonData);
      localStorage.setItem(this.storagePrefix + key, encrypted);
    } catch (error) {
      console.error('儲存失敗:', error);
    }
  }

  /**
   * 安全讀取資料
   */
  getItem<T>(key: string): T | null {
    try {
      const encrypted = localStorage.getItem(this.storagePrefix + key);
      if (!encrypted) return null;

      const decrypted = this.decrypt(encrypted);
      return JSON.parse(decrypted) as T;
    } catch (error) {
      console.error('讀取失敗:', error);
      return null;
    }
  }

  /**
   * 移除資料
   */
  removeItem(key: string): void {
    localStorage.removeItem(this.storagePrefix + key);
  }

  /**
   * 清空所有安全儲存的資料
   */
  clear(): void {
    Object.keys(localStorage)
      .filter(key => key.startsWith(this.storagePrefix))
      .forEach(key => localStorage.removeItem(key));
  }
}

// 使用範例
const storage = new SecureStorage('your-secret-key-here');

// 儲存敏感資料
storage.setItem('user_preferences', {
  theme: 'dark',
  notifications: true
});

// 讀取資料
const preferences = storage.getItem<{theme: string; notifications: boolean}>('user_preferences');

/**
 * ⚠️ 重要提醒:
 * 1. 不要在前端儲存真正敏感的資料 (如密碼、信用卡號)
 * 2. localStorage 可以被 XSS 攻擊讀取
 * 3. 使用 httpOnly Cookie 儲存驗證 Token
 * 4. 實際專案應使用 Web Crypto API 進行加密
 */

5. 第三方依賴安全管理

# 檢查專案中的安全漏洞
npm audit

# 自動修復可修復的漏洞
npm audit fix

# 查看詳細的漏洞報告
npm audit --json

# 使用 Snyk 進行更深入的安全掃描
npx snyk test

# 檢查套件的授權問題
npx license-checker
{
  "scripts": {
    "security-check": "npm audit && npx snyk test",
    "deps-update": "npx npm-check-updates -u",
    "precommit": "npm run security-check"
  },
  "devDependencies": {
    "snyk": "^1.1000.0",
    "npm-check-updates": "^16.0.0"
  }
}

🔧 進階應用與最佳實踐

完整的安全檢查清單

開發階段

  • [ ] 使用 ESLint 安全規則 (eslint-plugin-security)
  • [ ] 啟用 TypeScript strict 模式
  • [ ] 程式碼審查關注安全問題
  • [ ] 使用安全的第三方套件
  • [ ] 定期更新依賴套件

部署階段

  • [ ] 設定正確的 CSP Header
  • [ ] 啟用 HTTPS (強制 SSL)
  • [ ] 設定安全的 Cookie 屬性
  • [ ] 實作 CSRF 防護
  • [ ] 移除生產環境的 Source Map

執行時階段

  • [ ] 監控 CSP 違規報告
  • [ ] 追蹤安全相關錯誤
  • [ ] 定期進行安全稽核
  • [ ] 建立安全事件回應流程

ESLint 安全規則設定

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:security/recommended'
  ],
  plugins: ['security'],
  rules: {
    // 禁止使用 eval
    'no-eval': 'error',

    // 禁止使用 innerHTML
    'no-unsanitized/property': 'error',
    'no-unsanitized/method': 'error',

    // 警告使用 dangerouslySetInnerHTML
    'react/no-danger': 'warn',

    // 檢測不安全的正規表達式
    'security/detect-unsafe-regex': 'error',

    // 檢測可能的 SQL 注入
    'security/detect-non-literal-fs-filename': 'warn',

    // 禁止動態 require
    'security/detect-non-literal-require': 'error',
  }
};

安全 Header 完整設定

/**
 * 生產環境安全 Header 設定
 */
const helmet = require('helmet');

app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-{RANDOM}'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
    },
  },

  // 防止點擊劫持
  frameguard: {
    action: 'deny'
  },

  // 防止 MIME 類型嗅探
  noSniff: true,

  // 啟用 HSTS
  hsts: {
    maxAge: 31536000,      // 1 年
    includeSubDomains: true,
    preload: true
  },

  // 禁用 X-Powered-By header
  hidePoweredBy: true,

  // Referrer Policy
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  },

  // Permissions Policy
  permissionsPolicy: {
    features: {
      camera: ["'none'"],
      microphone: ["'none'"],
      geolocation: ["'self'"],
      payment: ["'self'"],
    }
  }
}));

實際專案整合範例

/**
 * 安全的 API 客戶端 - 整合所有安全實踐
 */
import axios, { AxiosInstance, AxiosError } from 'axios';
import DOMPurify from 'isomorphic-dompurify';

class SecureAPIClient {
  private client: AxiosInstance;
  private csrfToken: string | null = null;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      withCredentials: true,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
    this.initCSRFToken();
  }

  /**
   * 初始化 CSRF Token
   */
  private initCSRFToken(): void {
    const metaToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
    this.csrfToken = metaToken?.content || null;
  }

  /**
   * 設定請求/回應攔截器
   */
  private setupInterceptors(): void {
    // 請求攔截器
    this.client.interceptors.request.use(
      (config) => {
        // 添加 CSRF Token
        if (this.csrfToken && config.method !== 'get') {
          config.headers['X-CSRF-Token'] = this.csrfToken;
        }

        // 清理輸入資料
        if (config.data) {
          config.data = this.sanitizeData(config.data);
        }

        return config;
      },
      (error) => Promise.reject(error)
    );

    // 回應攔截器
    this.client.interceptors.response.use(
      (response) => {
        // 清理回應資料中的 HTML
        if (response.data) {
          response.data = this.sanitizeData(response.data);
        }
        return response;
      },
      (error: AxiosError) => {
        this.handleError(error);
        return Promise.reject(error);
      }
    );
  }

  /**
   * 清理資料中的危險內容
   */
  private sanitizeData(data: any): any {
    if (typeof data === 'string') {
      return DOMPurify.sanitize(data);
    }

    if (Array.isArray(data)) {
      return data.map(item => this.sanitizeData(item));
    }

    if (typeof data === 'object' && data !== null) {
      const sanitized: any = {};
      for (const [key, value] of Object.entries(data)) {
        sanitized[key] = this.sanitizeData(value);
      }
      return sanitized;
    }

    return data;
  }

  /**
   * 統一錯誤處理
   */
  private handleError(error: AxiosError): void {
    if (error.response?.status === 403) {
      console.error('CSRF 驗證失敗,請重新整理頁面');
    } else if (error.response?.status === 401) {
      console.error('未授權,請重新登入');
      // 導向登入頁
      window.location.href = '/login';
    } else {
      console.error('API 請求失敗:', error.message);
    }
  }

  /**
   * GET 請求
   */
  async get<T>(url: string, params?: any): Promise<T> {
    const response = await this.client.get<T>(url, { params });
    return response.data;
  }

  /**
   * POST 請求
   */
  async post<T>(url: string, data?: any): Promise<T> {
    const response = await this.client.post<T>(url, data);
    return response.data;
  }

  /**
   * PUT 請求
   */
  async put<T>(url: string, data?: any): Promise<T> {
    const response = await this.client.put<T>(url, data);
    return response.data;
  }

  /**
   * DELETE 請求
   */
  async delete<T>(url: string): Promise<T> {
    const response = await this.client.delete<T>(url);
    return response.data;
  }
}

// 使用範例
const apiClient = new SecureAPIClient('https://api.example.com');

export default apiClient;

📋 本日重點回顧

  1. 前端安全重要性: 前端是使用者接觸的第一道防線,安全漏洞會直接影響使用者資料和信任。現代 Web 應用的複雜性使得前端安全變得更加關鍵。

  2. XSS 防護策略: 通過輸入驗證、輸出編碼、使用 DOMPurify 清理 HTML、避免使用 innerHTML 和 eval 等危險 API,建立多層防護體系。現代框架如 React 提供了基礎防護,但開發者仍需保持警惕。

  3. CSRF 防護機制: 實作 CSRF Token 驗證、設定 SameSite Cookie 屬性、使用 HTTP-only Cookie 儲存敏感資訊。結合 Axios 攔截器實現自動化的 CSRF 防護。

  4. CSP 安全策略: Content Security Policy 是防止 XSS 的強大工具,通過限制資源載入來源、使用 nonce 機制、監控違規報告,建立完整的內容安全體系。

  5. 安全的資料儲存: 避免在前端儲存敏感資料,使用加密儲存非關鍵資訊,優先使用 httpOnly Cookie 儲存驗證 Token,了解 localStorage 和 sessionStorage 的安全限制。

  6. 依賴套件安全: 定期執行 npm audit 檢查漏洞,使用 Snyk 等工具進行深度掃描,及時更新有安全問題的依賴套件,建立自動化的安全檢查流程。

🎯 最佳實踐建議

✅ 推薦做法

  • 多層防禦策略: 不依賴單一安全措施,結合輸入驗證、輸出編碼、CSP、HTTPS 等多層防護
  • 自動化安全檢查: 在 CI/CD 流程中整合 npm audit、ESLint security 規則、自動化安全測試
  • 定期安全稽核: 每季度進行安全審查,檢查依賴套件漏洞,更新安全策略
  • 安全意識培訓: 團隊成員定期學習最新的安全威脅和防護技術
  • 最小權限原則: 只賦予必要的權限,限制第三方腳本的存取範圍
  • 監控和告警: 建立 CSP 違規報告、異常請求監控、安全事件告警機制

❌ 避免陷阱

  • 信任使用者輸入: 永遠不要相信任何使用者輸入,所有輸入都需要驗證和清理
  • 明文儲存敏感資料: 不要在 localStorage、sessionStorage 或客戶端程式碼中儲存密碼、API 金鑰等敏感資訊
  • 使用過時的套件: 忽視 npm audit 警告,使用有已知漏洞的舊版本套件
  • 禁用安全功能: 為了方便開發而禁用 CSP、CORS 等安全機制
  • 依賴前端驗證: 前端驗證只是使用者體驗,後端必須再次驗證所有輸入
  • 忽視 HTTPS: 在生產環境使用 HTTP 傳輸敏感資料

⚠️ 常見錯誤

  1. 使用 dangerouslySetInnerHTML 而不清理內容

    // ❌ 錯誤: 直接使用使用者輸入
    <div dangerouslySetInnerHTML={{ __html: userInput }} />
    
    // ✅ 正確: 使用 DOMPurify 清理
    <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
    
  2. 在 URL 中傳遞敏感資訊

    // ❌ 錯誤: 敏感資訊暴露在 URL 中
    fetch(`/api/user?password=${userPassword}`);
    
    // ✅ 正確: 使用 POST 請求 body
    fetch('/api/user', {
      method: 'POST',
      body: JSON.stringify({ password: userPassword })
    });
    
  3. 硬編碼 API 金鑰

    // ❌ 錯誤: API 金鑰暴露在前端程式碼中
    const API_KEY = 'sk_live_abc123xyz';
    
    // ✅ 正確: 通過後端 API 處理需要金鑰的請求
    // 前端不應該直接持有敏感金鑰
    

🤔 延伸思考

  1. 安全與使用者體驗的平衡: 如何在加強安全防護的同時,不影響使用者的正常使用體驗?例如,過於嚴格的 CSP 可能會影響第三方整合,如何找到適當的平衡點?

  2. 零信任架構: 在前端應用中如何實踐「永不信任,始終驗證」的原則?對於微前端架構,如何確保不同子應用之間的安全隔離?

  3. 新興威脅應對: 隨著 Web3、AI 等新技術的發展,前端安全面臨哪些新的挑戰?如何為智能合約互動、AI 生成內容等新場景建立安全防護?

  4. 實踐挑戰:

    • 在你的專案中實作完整的 CSP 策略,監控一週的違規報告
    • 使用 Snyk 掃描專案依賴,修復所有高危和中危漏洞
    • 建立自動化的安全測試流程,整合到 CI/CD 中
    • 對現有專案進行安全審計,列出所有潛在的安全風險並制定修復計劃

上一篇
測試驅動的現代化開發:單元測試到 E2E 的完整策略
下一篇
效能最佳化實戰:從首屏載入到執行時效能的全方位最佳化
系列文
前端工程師的 Modern Web 實踐之道17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言