安全不是功能,而是品質的基石。每個程式設計師都應該具備安全意識,在編碼時就考慮安全問題。

為什麼安全很重要?#

安全漏洞的影響#

一個安全漏洞可能導致:

  • 用戶資料洩露
  • 系統被入侵
  • 經濟損失
  • 聲譽損害
  • 法律責任

著名安全事件#

事件漏洞類型影響
Heartbleed緩衝區溢位數百萬網站受影響
Equifax 洩露未修補漏洞1.47 億用戶資料外洩
GoToFail編碼錯誤SSL 驗證失效

如何評估安全缺陷#

CVSS 評分系統#

Common Vulnerability Scoring System 是業界標準的漏洞評分系統:

等級分數處理優先級
嚴重9.0-10.0立即修復
7.0-8.9盡快修復
4.0-6.9計劃修復
0.1-3.9可接受風險

評估維度#

  1. 攻擊向量 - 遠端還是本地?
  2. 攻擊複雜度 - 是否容易利用?
  3. 所需權限 - 是否需要驗證?
  4. 影響範圍 - 機密性、完整性、可用性

整數運算的安全威脅#

整數溢位#

// 危險:可能溢位
int totalPrice = quantity * price;

// 安全:檢查溢位
try {
    int totalPrice = Math.multiplyExact(quantity, price);
} catch (ArithmeticException e) {
    // 處理溢位
}

Java 安全整數運算#

// Java 8+ 提供的安全方法
Math.addExact(a, b);      // 溢位時拋例外
Math.subtractExact(a, b);
Math.multiplyExact(a, b);
Math.incrementExact(a);
Math.decrementExact(a);
Math.negateExact(a);

類型轉換風險#

// 危險:大數轉小數可能丟失資訊
long bigNumber = 3_000_000_000L;
int smallNumber = (int) bigNumber; // 結果不是預期值!

// 安全:使用 toIntExact
try {
    int smallNumber = Math.toIntExact(bigNumber);
} catch (ArithmeticException e) {
    // 處理轉換失敗
}

陣列與集合的安全陷阱#

防禦性拷貝#

// 危險:直接暴露內部陣列
public class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = start;  // 危險!
        this.end = end;
    }

    public Date getStart() {
        return start;  // 危險!外部可修改
    }
}

// 安全:防禦性拷貝
public class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());  // 拷貝
        this.end = new Date(end.getTime());
    }

    public Date getStart() {
        return new Date(start.getTime());  // 拷貝
    }
}

使用不可變集合#

// 危險:回傳可變集合
public List<String> getNames() {
    return names;
}

// 安全:回傳不可變視圖
public List<String> getNames() {
    return Collections.unmodifiableList(names);
}

// 更安全:回傳拷貝
public List<String> getNames() {
    return new ArrayList<>(names);
}

// Java 9+:使用不可變工廠方法
List<String> immutable = List.of("a", "b", "c");

敏感資訊處理#

敏感資訊類型#

  • 密碼、金鑰
  • 個人身份資訊(PII)
  • 信用卡號
  • 健康資訊

處理原則#

1. 最小化保存

// 不要記錄敏感資訊
logger.info("User login: " + username + ", password: " + password); // 危險!
logger.info("User login: " + username); // 安全

2. 使用 char[] 而非 String 存儲密碼

// String 是不可變的,會在記憶體中駐留
String password = "secret"; // 不推薦

// char[] 可以被清除
char[] password = new char[]{'s','e','c','r','e','t'};
try {
    authenticate(password);
} finally {
    Arrays.fill(password, '\0'); // 清除
}

3. 加密存儲

// 密碼應該雜湊存儲,不是加密
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());

// 驗證時比較雜湊值
if (BCrypt.checkpw(inputPassword, hashedPassword)) {
    // 密碼正確
}

繼承的安全缺陷#

問題#

子類別可以破壞父類別的約定:

public class SecureList<E> extends ArrayList<E> {
    @Override
    public boolean add(E e) {
        // 子類別可以繞過安全檢查
        return super.add(e);
    }
}

解決方案#

1. 使用 final 防止繼承

public final class ImmutablePoint {
    private final int x;
    private final int y;
}

2. 優先使用組合而非繼承

// 使用組合
public class SecureList<E> {
    private final List<E> list = new ArrayList<>();

    public boolean add(E e) {
        // 完全控制行為
        if (isValid(e)) {
            return list.add(e);
        }
        return false;
    }
}

信任邊界#

什麼是信任邊界?#

信任邊界劃分了可信區域和不可信區域。來自不可信區域的資料必須驗證。

flowchart LR
    subgraph 不可信區域
        U[用戶輸入]
        A[外部 API]
        F[檔案上傳]
    end

    subgraph 信任邊界
        V[驗證層]
    end

    subgraph 可信區域
        B[業務邏輯]
    end

    U --> V
    A --> V
    F --> V
    V --> B

    style 不可信區域 fill:#ffcdd2
    style 信任邊界 fill:#fff9c4
    style 可信區域 fill:#c8e6c9

輸入驗證原則#

永遠不要信任外部輸入

public void processInput(String input) {
    // 1. 非空檢查
    if (input == null || input.isEmpty()) {
        throw new IllegalArgumentException("Input cannot be empty");
    }

    // 2. 長度限制
    if (input.length() > MAX_LENGTH) {
        throw new IllegalArgumentException("Input too long");
    }

    // 3. 格式驗證
    if (!VALID_PATTERN.matcher(input).matches()) {
        throw new IllegalArgumentException("Invalid format");
    }

    // 4. 編碼/轉義
    String safeInput = encode(input);

    // 現在可以安全使用
    process(safeInput);
}

常見攻擊與防禦#

攻擊類型防禦措施
SQL 注入使用參數化查詢
XSSHTML 編碼輸出
路徑遍歷白名單驗證路徑
命令注入避免 shell 命令,使用 API
// SQL 注入防禦
// 危險
String sql = "SELECT * FROM users WHERE name = '" + name + "'";

// 安全:參數化查詢
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?");
stmt.setString(1, name);

序列化的危害#

風險#

Java 序列化存在嚴重的安全問題:

  • 反序列化可執行任意程式碼
  • 繞過建構函式
  • 破壞單例模式

Java 原生序列化是安全隱患的主要來源,應盡量避免使用。

安全替代方案#

// 避免:Java 原生序列化
ObjectOutputStream.writeObject(obj);
ObjectInputStream.readObject();

// 推薦:使用 JSON
String json = objectMapper.writeValueAsString(obj);
MyClass obj = objectMapper.readValue(json, MyClass.class);

// 推薦:使用 Protocol Buffers
byte[] bytes = myProto.toByteArray();
MyProto proto = MyProto.parseFrom(bytes);

如果必須使用序列化#

public class SafeClass implements Serializable {
    private static final long serialVersionUID = 1L;

    // 使用序列化代理
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 防止直接反序列化
    private void readObject(ObjectInputStream stream)
            throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        // 代理實作
    }
}

程式碼權限控制#

最小權限原則#

程式碼應該只擁有完成任務所需的最小權限。

// 不好:請求所有權限
System.setSecurityManager(null);

// 好:精確指定所需權限
grant codeBase "file:/app/lib/*" {
    permission java.io.FilePermission "/data/-", "read";
    permission java.net.SocketPermission "api.example.com:443", "connect";
};

敏感操作保護#

public void sensitiveOperation() {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new MyPermission("sensitive"));
    }
    // 執行敏感操作
}

縱深防禦策略#

不依賴單一防線,建立多層防禦:

flowchart TB
    L1[第一層:輸入驗證] --> L2[第二層:認證授權]
    L2 --> L3[第三層:安全編碼]
    L3 --> L4[第四層:加密傳輸]
    L4 --> L5[第五層:日誌審計]
    L5 --> L6[第六層:監控告警]

    style L1 fill:#ffcdd2
    style L2 fill:#ffe0b2
    style L3 fill:#fff9c4
    style L4 fill:#c8e6c9
    style L5 fill:#b3e5fc
    style L6 fill:#e1bee7

每一層都假設其他層可能失效。

安全編碼檢查清單#

安全編碼清單

輸入處理

  • 驗證所有外部輸入
  • 使用白名單而非黑名單
  • 限制輸入長度
  • 編碼/轉義輸出

資料保護

  • 敏感資料加密存儲
  • 密碼使用雜湊(bcrypt/scrypt)
  • 傳輸使用 TLS
  • 不在日誌中記錄敏感資訊

認證授權

  • 實施最小權限原則
  • 工作階段逾時機制
  • 防止暴力破解

程式碼安全

  • 使用安全的整數運算
  • 防禦性拷貝
  • 避免原生序列化
  • 及時釋放資源

錯誤處理

  • 不暴露堆疊追蹤給用戶
  • 記錄安全相關事件
  • 實施適當的錯誤訊息

最佳實踐總結#

  1. 安全是設計問題,不是測試問題 - 從設計階段就考慮安全
  2. 縱深防禦 - 不依賴單一防線
  3. 最小權限 - 只給必要的權限
  4. 不信任原則 - 驗證所有外部輸入
  5. 保持更新 - 及時修補已知漏洞
  6. 安全審計 - 定期 Code Review 關注安全

安全是每個開發者的責任,不只是安全團隊的事。