安全不是功能,而是品質的基石。每個程式設計師都應該具備安全意識,在編碼時就考慮安全問題。
為什麼安全很重要?#
安全漏洞的影響#
一個安全漏洞可能導致:
- 用戶資料洩露
- 系統被入侵
- 經濟損失
- 聲譽損害
- 法律責任
著名安全事件#
| 事件 | 漏洞類型 | 影響 |
|---|---|---|
| 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 | 可接受風險 |
評估維度#
- 攻擊向量 - 遠端還是本地?
- 攻擊複雜度 - 是否容易利用?
- 所需權限 - 是否需要驗證?
- 影響範圍 - 機密性、完整性、可用性
整數運算的安全威脅#
整數溢位#
// 危險:可能溢位
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 注入 | 使用參數化查詢 |
| XSS | HTML 編碼輸出 |
| 路徑遍歷 | 白名單驗證路徑 |
| 命令注入 | 避免 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
- 不在日誌中記錄敏感資訊
認證授權
- 實施最小權限原則
- 工作階段逾時機制
- 防止暴力破解
程式碼安全
- 使用安全的整數運算
- 防禦性拷貝
- 避免原生序列化
- 及時釋放資源
錯誤處理
- 不暴露堆疊追蹤給用戶
- 記錄安全相關事件
- 實施適當的錯誤訊息
最佳實踐總結#
- 安全是設計問題,不是測試問題 - 從設計階段就考慮安全
- 縱深防禦 - 不依賴單一防線
- 最小權限 - 只給必要的權限
- 不信任原則 - 驗證所有外部輸入
- 保持更新 - 及時修補已知漏洞
- 安全審計 - 定期 Code Review 關注安全
安全是每個開發者的責任,不只是安全團隊的事。