安全程式碼指引

目錄

  1. 文件目的
  2. 通用安全開發原則
  3. 程式語言安全指引
  4. OWASP Top 10 對應對策
  5. API 安全設計
  6. 容器化與雲端安全
  7. CI/CD 安全
  8. 安全測試與驗證
  9. 資料保護與隱私
  10. 事件回應與復原
  11. 日常開發檢查清單 (Checklist)
  12. 常見錯誤與反例
  13. 合規性與法規要求
  14. 延伸資源

1. 文件目的

安全程式碼的撰寫是每位開發者的基本職責。良好的安全設計不僅能保護公司與用戶資料,避免資安事件造成商譽損失與法律責任,也有助於符合法規(如 GDPR、個資法)及客戶合約要求。安全程式碼能降低維運成本、減少漏洞修補時間,讓業務更穩健發展。

實務案例:某金融業者因 SQL Injection 漏洞導致客戶資料外洩,最終被主管機關重罰並失去客戶信任。


2. 通用安全開發原則

2.1 最小權限原則

  • 僅授權必要權限給帳號、程式與服務。
  • 例:資料庫帳號只給查詢權限,禁止 DROP/DELETE。

2.2 輸入驗證

  • 所有外部輸入(表單、API、檔案)都必須驗證型別、長度、格式與範圍。
  • 優先採用白名單(允許清單)驗證。
  • 範例(Java):
    if (!input.matches("^[a-zA-Z0-9_]{3,20}$")) {
        throw new IllegalArgumentException("輸入格式錯誤");
    }

2.3 錯誤處理

  • 不回傳詳細錯誤訊息給前端,避免洩漏系統資訊。
  • 記錄錯誤日誌,方便追蹤。
  • 範例(Python):
    try:
        # ...
    except Exception as e:
        logger.error(f"Error: {e}")
        return "系統發生錯誤,請聯絡管理員"

2.4 加密使用原則

  • 儲存敏感資料(密碼、Token)必須加密或雜湊。
  • 傳輸敏感資料必須使用 HTTPS/TLS。
  • 禁止自創加密演算法,應使用業界標準(如 bcrypt、AES)。
  • 範例(Java 密碼雜湊):
    String hash = BCrypt.hashpw(password, BCrypt.gensalt());

注意事項

  • 切勿將密碼、金鑰、Token 寫死在程式碼中,應使用環境變數或安全憑證管理服務。

2.5 安全標頭設定

重要性:HTTP 安全標頭是防護用戶端攻擊的重要機制。

常用安全標頭

  • Content-Security-Policy (CSP):防護 XSS 攻擊
  • X-Frame-Options:防護點擊劫持
  • X-Content-Type-Options:防護 MIME 類型混淆
  • Strict-Transport-Security (HSTS):強制 HTTPS
  • X-XSS-Protection:啟用瀏覽器 XSS 防護

程式碼範例(Spring Boot)

@Configuration
public class SecurityHeadersConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                .frameOptions().deny()
                .contentTypeOptions().and()
                .httpStrictTransportSecurity(hstsConfig -> hstsConfig
                    .maxAgeInSeconds(31536000)
                    .includeSubdomains(true))
                .and())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

CSP 設定範例

<!-- 基本 CSP 設定 -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline'; 
               style-src 'self' 'unsafe-inline'; 
               img-src 'self' data: https:;">

2.6 Session 與 Token 管理

Session 安全設定

  • 使用安全的 Session ID 產生機制
  • 設定適當的 Session 逾時時間
  • 登出時清除 Session
  • 使用 HttpOnly 和 Secure 標誌

JWT Token 安全使用

  • 使用強簽名演算法(RS256, ES256)
  • 設定適當的過期時間
  • 實作 Token 撤銷機制
  • 敏感資料不放入 JWT payload

程式碼範例(Spring Boot Session)

@Configuration
public class SessionConfig {
    
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        serializer.setHttpOnly(true);
        serializer.setSecure(true); // HTTPS only
        serializer.setSameSite("Strict");
        return serializer;
    }
}

JWT 實作範例

@Service
public class JwtService {
    
    private final String SECRET_KEY = getFromEnvironment("JWT_SECRET");
    private final long EXPIRATION_TIME = 3600000; // 1 hour
    
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

3. 程式語言安全指引

3.1 Java / Spring Boot

  • 輸入驗證:使用 Bean Validation(@Valid, @NotNull, @Size 等)。
  • SQL 操作:一律使用 PreparedStatement 或 ORM,避免 SQL Injection。
  • XSS 防護:前端輸出時進行 HTML Escape。
  • 檔案上傳:檢查副檔名與 MIME type,限制檔案大小。
  • 日誌:避免記錄敏感資訊(如密碼、信用卡號)。

常見漏洞與安全寫法

  • 錯誤範例:
    String sql = "SELECT * FROM user WHERE name = '" + name + "'";
    stmt.executeQuery(sql);
  • 正確範例:
    String sql = "SELECT * FROM user WHERE name = ?";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setString(1, name);
    ps.executeQuery();

注意事項

  • Spring Boot 預設開啟 CSRF 防護,請勿隨意關閉。

3.2 Python

常見安全問題

  • 命令注入:避免使用 eval(), exec(), os.system()
  • 路徑穿越:檢查檔案路徑,防止 ../../../etc/passwd 攻擊。
  • 反序列化漏洞:避免使用 pickle.loads() 處理不可信資料。

安全寫法範例

  • 錯誤範例(命令注入):

    import os
    user_input = request.form['cmd']
    os.system(f"ls {user_input}")  # 危險!
  • 正確範例:

    import subprocess
    import shlex
    user_input = request.form['cmd']
    # 使用白名單驗證
    if user_input not in ['logs', 'data', 'config']:
        return "無效指令"
    subprocess.run(['ls', user_input], check=True)
  • 檔案讀取安全範例:

    import os.path
    
    def safe_read_file(filename):
        # 防止路徑穿越攻擊
        if '..' in filename or filename.startswith('/'):
            raise ValueError("無效檔案路徑")
    
        safe_path = os.path.join('/allowed/directory', filename)
        if not safe_path.startswith('/allowed/directory'):
            raise ValueError("路徑穿越攻擊")
    
        with open(safe_path, 'r') as f:
            return f.read()

3.3 JavaScript / TypeScript / Vue3

前端安全重點

  • XSS 防護:使用框架內建的安全機制,避免 innerHTML
  • CSRF 防護:使用 CSRF Token。
  • 敏感資料:前端程式碼不可包含 API Key、密碼等機密。

Vue3 安全寫法

  • 錯誤範例(XSS 風險):

    // 危險!直接插入 HTML
    <div v-html="userContent"></div>
  • 正確範例:

    // 安全:Vue 自動 escape
    <div>{{ userContent }}</div>
    
    // 或使用 DOMPurify 清理 HTML
    import DOMPurify from 'dompurify'
    
    computed: {
      sanitizedContent() {
        return DOMPurify.sanitize(this.userContent)
      }
    }
  • HTTP 請求安全範例:

    // 使用 HTTPS 並設定安全標頭
    const response = await fetch('/api/data', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken,
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: JSON.stringify(data)
    })

注意事項

  • 永遠不要相信前端驗證,後端必須重新驗證所有輸入。
  • 使用 Content Security Policy (CSP) 減少 XSS 風險。

3.4 資料庫安全

基本安全原則

  • 使用最小權限資料庫帳號
  • 啟用資料庫稽核日誌
  • 定期備份與測試復原
  • 加密敏感資料欄位
  • 定期更新資料庫軟體

連線安全設定

// Spring Boot 資料庫連線安全設定
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=true&requireSSL=true&verifyServerCertificate=true
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 20000
      idle-timeout: 300000
      max-lifetime: 1200000

資料加密範例

@Entity
public class User {
    @Id
    private Long id;
    
    private String username;
    
    // 敏感資料加密儲存
    @Convert(converter = EncryptedStringConverter.class)
    private String email;
    
    @Convert(converter = EncryptedStringConverter.class)
    private String phoneNumber;
    
    // 密碼雜湊
    @Column(name = "password_hash")
    private String passwordHash;
}

@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
    
    @Override
    public String convertToDatabaseColumn(String attribute) {
        if (attribute == null) return null;
        return encryptionService.encrypt(attribute);
    }
    
    @Override
    public String convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        return encryptionService.decrypt(dbData);
    }
}

查詢安全最佳實務

// 使用 Repository 模式避免動態查詢
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.createDate >= :startDate")
    List<User> findActiveUsersAfterDate(@Param("status") String status, 
                                       @Param("startDate") LocalDateTime startDate);
    
    // 避免使用 nativeQuery,如必要使用也要參數化
    @Query(value = "SELECT * FROM users WHERE department_id = ? AND salary > ?", 
           nativeQuery = true)
    List<User> findByDepartmentAndSalary(Long departmentId, BigDecimal minSalary);
}

資料庫權限控制

-- 建立專用資料庫使用者(最小權限原則)
CREATE USER 'app_read'@'%' IDENTIFIED BY 'strong_password';
CREATE USER 'app_write'@'%' IDENTIFIED BY 'strong_password';

-- 授權特定權限
GRANT SELECT ON mydb.* TO 'app_read'@'%';
GRANT SELECT, INSERT, UPDATE ON mydb.users TO 'app_write'@'%';
GRANT SELECT, INSERT, UPDATE ON mydb.orders TO 'app_write'@'%';

-- 禁止危險操作
-- 不授權 DROP, DELETE, ALTER 等權限給應用程式帳號

4. OWASP Top 10 對應對策

4.1 A01:2021 – Broken Access Control (存取控制失效)

風險說明:用戶能存取未授權的功能或資料。

防護措施

  • 實作角色權限控制(RBAC)
  • 每個 API 都要檢查用戶權限
  • 預設拒絕存取

程式碼範例(Spring Boot)

@PreAuthorize("hasRole('ADMIN') or @userService.isOwner(authentication.name, #userId)")
@GetMapping("/user/{userId}")
public User getUser(@PathVariable Long userId) {
    return userService.findById(userId);
}

4.2 A02:2021 – Cryptographic Failures (加密失效)

風險說明:敏感資料未加密或使用弱加密。

防護措施

  • 使用強加密演算法(AES-256, RSA-2048+)
  • 敏感資料傳輸使用 HTTPS/TLS 1.2+
  • 密碼使用安全雜湊(bcrypt, Argon2)

程式碼範例(Java)

// 密碼雜湊
String hashedPassword = BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));

// AES 加密
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal(data.getBytes());

4.3 A03:2021 – Injection (注入攻擊)

風險說明:SQL、NoSQL、OS、LDAP 注入攻擊。

防護措施

  • 使用參數化查詢
  • 輸入驗證與過濾
  • 最小權限資料庫帳號

程式碼範例

// SQL Injection 防護
@Query("SELECT u FROM User u WHERE u.email = :email")
User findByEmail(@Param("email") String email);

// 輸入驗證
@Valid
@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
private String email;

4.4 A04:2021 – Insecure Design (不安全設計)

風險說明:系統架構設計缺乏安全考量。

防護措施

  • 威脅建模(Threat Modeling)
  • 安全設計模式
  • 多層防禦

4.5 A05:2021 – Security Misconfiguration (安全設定錯誤)

風險說明:預設設定、錯誤設定、未更新元件。

防護措施

  • 移除預設帳號與範例程式
  • 定期更新與修補
  • 最小化安裝與設定

程式碼範例(Spring Boot)

# application-prod.yml
server:
  error:
    include-stacktrace: never
    include-message: never
management:
  endpoints:
    enabled-by-default: false

4.6 A06:2021 – Vulnerable Components (易受攻擊元件)

風險說明:使用有漏洞的第三方套件。

防護措施

  • 定期掃描相依性漏洞
  • 及時更新套件版本
  • 移除不使用的相依性

4.7 A07:2021 – Identification and Authentication Failures (識別與驗證失效)

風險說明:弱密碼、認證繞過、Session 管理問題。

防護措施

  • 強制密碼複雜度
  • 多因子驗證(MFA)
  • Session 逾時與安全設定

程式碼範例(密碼驗證)

@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
         message = "密碼必須包含大小寫字母、數字、特殊字元,長度至少8字元")
private String password;

4.8 A08:2021 – Software and Data Integrity Failures (軟體與資料完整性失效)

風險說明:未驗證軟體更新、CI/CD 管線、反序列化攻擊。

防護措施

  • 數位簽章驗證
  • 安全的 CI/CD 管線
  • 避免不安全的反序列化

4.9 A09:2021 – Security Logging & Monitoring Failures (安全日誌與監控失效)

風險說明:缺乏日誌記錄、監控與回應機制。

防護措施

  • 記錄安全事件(登入失敗、權限異常)
  • 即時監控與告警
  • 日誌保護與備份

程式碼範例(日誌記錄)

@EventListener
public void handleLoginFailure(AuthenticationFailureBadCredentialsEvent event) {
    String username = event.getAuthentication().getName();
    logger.warn("登入失敗: 用戶={}, IP={}, 時間={}", 
                username, getClientIP(), LocalDateTime.now());
}

4.10 A10:2021 – Server-Side Request Forgery (SSRF)

風險說明:伺服器端請求偽造攻擊。

防護措施

  • 白名單驗證 URL
  • 網路隔離
  • 禁止存取內網資源

程式碼範例

public boolean isAllowedUrl(String url) {
    try {
        URL parsedUrl = new URL(url);
        String host = parsedUrl.getHost();
        
        // 白名單檢查
        return allowedHosts.contains(host) && 
               !isPrivateIP(host);
    } catch (Exception e) {
        return false;
    }
}

5. API 安全設計

5.1 RESTful API 安全

認證與授權

  • 使用標準認證機制(OAuth 2.0, JWT)
  • 實作細粒度權限控制
  • API Key 管理與輪替
  • 速率限制與節流

輸入驗證與過濾

@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
    
    @PostMapping
    public ResponseEntity<User> createUser(
            @Valid @RequestBody CreateUserRequest request,
            @RequestHeader("Authorization") String authHeader) {
        
        // 驗證授權標頭
        if (!jwtService.validateToken(extractToken(authHeader))) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 檢查權限
        if (!hasPermission("USER_CREATE")) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        
        User user = userService.createUser(request);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping("/{userId}")
    @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(authentication.name, #userId)")
    public ResponseEntity<User> getUser(@PathVariable @Min(1) Long userId) {
        User user = userService.findById(userId);
        return ResponseEntity.ok(sanitizeUserData(user));
    }
}

回應資料過濾

@JsonView(UserViews.Public.class)
public class User {
    @JsonView(UserViews.Public.class)
    private Long id;
    
    @JsonView(UserViews.Public.class)
    private String username;
    
    @JsonView(UserViews.Internal.class)
    private String email;
    
    @JsonView(UserViews.Admin.class)
    private String passwordHash;
}

public class UserViews {
    public static class Public {}
    public static class Internal extends Public {}
    public static class Admin extends Internal {}
}

5.2 GraphQL 安全

查詢深度限制

@Configuration
public class GraphQLConfig {
    
    @Bean
    public QueryExecutionStrategy queryExecutionStrategy() {
        return new AsyncExecutionStrategy(new SimpleDataFetcherExceptionHandler()) {
            @Override
            public CompletableFuture<ExecutionResult> execute(
                    ExecutionContext executionContext,
                    FieldSubSelection fieldSubSelection) throws NonNullableFieldWasNullException {
                
                // 檢查查詢深度
                int depth = calculateQueryDepth(executionContext.getOperationDefinition());
                if (depth > MAX_QUERY_DEPTH) {
                    throw new RuntimeException("Query depth exceeds maximum allowed: " + MAX_QUERY_DEPTH);
                }
                
                return super.execute(executionContext, fieldSubSelection);
            }
        };
    }
}

欄位級權限控制

@Component
public class UserDataFetcher implements DataFetcher<User> {
    
    @Override
    public User get(DataFetchingEnvironment environment) throws Exception {
        Long userId = environment.getArgument("id");
        SecurityContext securityContext = environment.getContext();
        
        User user = userService.findById(userId);
        
        // 根據權限過濾敏感欄位
        if (!hasAdminRole(securityContext)) {
            user.setEmail(null);
            user.setPhoneNumber(null);
        }
        
        return user;
    }
}

5.3 API 版本控制與向後相容

版本控制策略

// URL 版本控制
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
    // v1 實作
}

@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
    // v2 實作,保持向後相容
}

// Header 版本控制
@GetMapping(value = "/users", headers = "API-Version=1")
public ResponseEntity<List<UserV1>> getUsersV1() {
    return ResponseEntity.ok(userService.getUsersV1());
}

@GetMapping(value = "/users", headers = "API-Version=2")
public ResponseEntity<List<UserV2>> getUsersV2() {
    return ResponseEntity.ok(userService.getUsersV2());
}

廢棄 API 處理

@RestController
public class DeprecatedApiController {
    
    @GetMapping("/api/old-endpoint")
    @Deprecated
    public ResponseEntity<String> oldEndpoint(HttpServletResponse response) {
        // 新增廢棄警告標頭
        response.setHeader("Warning", "299 - \"This API version is deprecated\"");
        response.setHeader("Sunset", "2025-12-31T23:59:59Z");
        response.setHeader("Link", "</api/v2/new-endpoint>; rel=\"successor-version\"");
        
        return ResponseEntity.ok("Legacy response");
    }
}

5.4 API 速率限制與節流

Redis 基礎速率限制

@Component
public class RateLimitingInterceptor implements HandlerInterceptor {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String RATE_LIMIT_KEY_PREFIX = "rate_limit:";
    private static final int REQUESTS_PER_MINUTE = 100;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String clientId = getClientId(request);
        String key = RATE_LIMIT_KEY_PREFIX + clientId;
        
        String currentCount = redisTemplate.opsForValue().get(key);
        if (currentCount == null) {
            redisTemplate.opsForValue().set(key, "1", Duration.ofMinutes(1));
        } else {
            int count = Integer.parseInt(currentCount);
            if (count >= REQUESTS_PER_MINUTE) {
                response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                response.setHeader("Retry-After", "60");
                return false;
            }
            redisTemplate.opsForValue().increment(key);
        }
        
        return true;
    }
    
    private String getClientId(HttpServletRequest request) {
        // 可以基於 IP、API Key 或使用者 ID
        String apiKey = request.getHeader("X-API-Key");
        return apiKey != null ? apiKey : request.getRemoteAddr();
    }
}

滑動視窗速率限制

@Service
public class SlidingWindowRateLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean isAllowed(String key, int maxRequests, Duration window) {
        long now = System.currentTimeMillis();
        long windowStart = now - window.toMillis();
        
        String luaScript = """
            local key = KEYS[1]
            local window_start = ARGV[1]
            local current_time = ARGV[2]
            local max_requests = tonumber(ARGV[3])
            
            -- 移除過期的請求記錄
            redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
            
            -- 計算當前視窗內的請求數
            local current_requests = redis.call('ZCARD', key)
            
            if current_requests < max_requests then
                -- 新增當前請求
                redis.call('ZADD', key, current_time, current_time)
                redis.call('EXPIRE', key, 3600) -- 1小時過期
                return 1
            else
                return 0
            end
        """;
        
        Long result = redisTemplate.execute(
            RedisScript.of(luaScript, Long.class),
            Collections.singletonList(key),
            String.valueOf(windowStart),
            String.valueOf(now),
            String.valueOf(maxRequests)
        );
        
        return result != null && result == 1;
    }
}

6. 容器化與雲端安全

6.1 Docker 安全

安全映像檔建置

# 使用官方基礎映像檔
FROM openjdk:17-jre-slim

# 建立非 root 使用者
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 設定工作目錄
WORKDIR /app

# 複製應用程式檔案
COPY --chown=appuser:appuser target/app.jar app.jar

# 移除不必要的套件
RUN apt-get update && \
    apt-get remove -y wget curl && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

# 切換到非特權使用者
USER appuser

# 暴露必要端口
EXPOSE 8080

# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# 啟動應用程式
ENTRYPOINT ["java", "-jar", "app.jar"]

容器安全掃描

# 使用 Trivy 掃描映像檔漏洞
trivy image myapp:latest

# 使用 Docker Bench Security 檢查 Docker 安全設定
docker run --rm --net host --pid host --userns host --cap-add audit_control \
    -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
    -v /etc:/etc:ro \
    -v /var/lib:/var/lib:ro \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    --label docker_bench_security \
    docker/docker-bench-security

6.2 Kubernetes 安全

Pod 安全策略

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
  containers:
  - name: app
    image: myapp:latest
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL
        add:
        - NET_BIND_SERVICE
    resources:
      limits:
        memory: "512Mi"
        cpu: "500m"
      requests:
        memory: "256Mi"
        cpu: "250m"
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: var-run
      mountPath: /var/run
  volumes:
  - name: tmp
    emptyDir: {}
  - name: var-run
    emptyDir: {}

網路政策

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-netpol
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432

6.3 雲端服務安全

AWS S3 安全設定

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyInsecureConnections",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::my-secure-bucket",
                "arn:aws:s3:::my-secure-bucket/*"
            ],
            "Condition": {
                "Bool": {
                    "aws:SecureTransport": "false"
                }
            }
        },
        {
            "Sid": "AllowApplicationAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT-ID:role/MyAppRole"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::my-secure-bucket/app-data/*"
        }
    ]
}

Azure Key Vault 整合

@Configuration
public class KeyVaultConfig {
    
    @Bean
    public SecretClient secretClient() {
        return new SecretClientBuilder()
            .vaultUrl("https://my-keyvault.vault.azure.net/")
            .credential(new DefaultAzureCredentialBuilder().build())
            .buildClient();
    }
    
    @Value("${azure.keyvault.secret.database-password}")
    private String databasePassword;
}

7. CI/CD 安全

7.1 代碼儲存庫安全

分支保護規則

# .github/branch-protection.yml
branches:
  main:
    protection:
      required_status_checks:
        strict: true
        contexts:
          - "security-scan"
          - "unit-tests"
          - "integration-tests"
      enforce_admins: true
      required_pull_request_reviews:
        required_approving_review_count: 2
        dismiss_stale_reviews: true
        require_code_owner_reviews: true
      restrictions:
        users: []
        teams: ["security-team"]

敏感資料掃描

# .github/workflows/security-scan.yml
name: Security Scan
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  secret-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    
    - name: Run TruffleHog
      uses: trufflesecurity/trufflehog@main
      with:
        path: ./
        base: main
        head: HEAD
        extra_args: --debug --only-verified

7.2 建構流程安全

安全建構配置

# .github/workflows/build.yml
name: Secure Build
on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    
    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    
    - name: Run OWASP Dependency Check
      run: mvn org.owasp:dependency-check-maven:check
    
    - name: Run SonarQube Scan
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      run: mvn sonar:sonar
    
    - name: Build with Maven
      run: mvn clean compile test package
    
    - name: Build Docker image
      run: |
        docker build -t myapp:${{ github.sha }} .
    
    - name: Scan Docker image
      run: |
        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
          -v $PWD:/tmp/.cache/ aquasec/trivy image myapp:${{ github.sha }}

7.3 部署安全

安全部署策略

# deploy.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/myapp-config
    targetRevision: HEAD
    path: k8s
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    - PruneLast=true
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas

7.4 供應鏈安全

依賴項驗證

<!-- pom.xml -->
<plugin>
    <groupId>org.cyclonedx</groupId>
    <artifactId>cyclonedx-maven-plugin</artifactId>
    <version>2.7.9</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>makeAggregateBom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>8.4.0</version>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>
        <suppressionFiles>
            <suppressionFile>dependency-check-suppressions.xml</suppressionFile>
        </suppressionFiles>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

軟體物料清單 (SBOM)

# 生成 SBOM
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom

# 驗證 SBOM
syft packages . -o spdx-json=sbom.spdx.json
grype sbom:sbom.spdx.json

8. 安全測試與驗證

8.1 靜態應用程式安全測試 (SAST)

SonarQube 整合

<!-- pom.xml -->
<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>3.9.1.2184</version>
</plugin>
# sonar-project.properties
sonar.projectKey=my-java-project
sonar.projectName=My Java Project
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.java.checkstyle.reportPaths=target/checkstyle-result.xml
sonar.java.pmd.reportPaths=target/pmd.xml
sonar.java.spotbugs.reportPaths=target/spotbugsXml.xml

代碼品質檢查

// 使用 SpotBugs 註解避免誤報
@SuppressFBWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING", 
                    justification = "SQL is constructed safely using QueryBuilder")
public List<User> findUsers(String sortField, String sortDirection) {
    // 安全的動態查詢建構
    String sql = queryBuilder.buildQuery(sortField, sortDirection);
    return jdbcTemplate.query(sql, userRowMapper);
}

8.2 動態應用程式安全測試 (DAST)

OWASP ZAP 整合

# .github/workflows/dast.yml
name: DAST Scan
on:
  schedule:
    - cron: '0 2 * * *'  # 每日凌晨 2 點執行

jobs:
  dast:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    
    - name: Start application
      run: |
        docker-compose up -d
        sleep 30  # 等待應用程式啟動
    
    - name: Run OWASP ZAP Baseline Scan
      uses: zaproxy/action-baseline@v0.7.0
      with:
        target: 'http://localhost:8080'
        rules_file_name: '.zap/rules.tsv'
        cmd_options: '-a'

8.3 互動式應用程式安全測試 (IAST)

Contrast Security 範例

# 下載 Contrast Agent
curl -o contrast.jar https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.contrastsecurity&a=contrast-agent&v=LATEST

# 啟動應用程式時載入 Agent
java -javaagent:contrast.jar -jar myapp.jar

8.4 滲透測試

自動化滲透測試腳本

# pentest_automation.py
import requests
import json
from urllib.parse import urljoin

class SecurityTester:
    def __init__(self, base_url):
        self.base_url = base_url
        self.session = requests.Session()
    
    def test_sql_injection(self, endpoint, params):
        """測試 SQL Injection 漏洞"""
        payloads = [
            "1' OR '1'='1",
            "1'; DROP TABLE users; --",
            "1' UNION SELECT username, password FROM users --"
        ]
        
        for payload in payloads:
            test_params = params.copy()
            for key in test_params:
                test_params[key] = payload
                
            response = self.session.get(
                urljoin(self.base_url, endpoint),
                params=test_params
            )
            
            # 檢查 SQL 錯誤訊息
            if any(error in response.text.lower() for error in 
                   ['sql', 'mysql', 'postgres', 'oracle']):
                print(f"Potential SQL Injection: {endpoint} with {test_params}")
    
    def test_xss(self, endpoint, params):
        """測試 XSS 漏洞"""
        payloads = [
            "<script>alert('XSS')</script>",
            "javascript:alert('XSS')",
            "<img src=x onerror=alert('XSS')>"
        ]
        
        for payload in payloads:
            test_params = params.copy()
            for key in test_params:
                test_params[key] = payload
                
            response = self.session.get(
                urljoin(self.base_url, endpoint),
                params=test_params
            )
            
            if payload in response.text:
                print(f"Potential XSS: {endpoint} with {test_params}")

# 使用範例
tester = SecurityTester("http://localhost:8080")
tester.test_sql_injection("/api/users", {"id": "1"})
tester.test_xss("/search", {"q": "test"})

9. 資料保護與隱私

9.1 個人資料保護

個資識別與分類

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    @PersonalData(category = DataCategory.IDENTITY)
    private String username;
    
    @PersonalData(category = DataCategory.CONTACT, sensitive = true)
    @Convert(converter = EncryptedStringConverter.class)
    private String email;
    
    @PersonalData(category = DataCategory.SENSITIVE)
    @Convert(converter = EncryptedStringConverter.class)
    private String phoneNumber;
    
    @PersonalData(category = DataCategory.BIOMETRIC, 
                  retention = @Retention(period = 90, unit = ChronoUnit.DAYS))
    private String fingerprint;
}

@Retention
@Target({ElementType.FIELD})
public @interface Retention {
    int period();
    ChronoUnit unit();
}

同意管理

@Entity
public class ConsentRecord {
    @Id
    private Long id;
    
    private Long userId;
    private String purpose;
    private ConsentStatus status;
    private LocalDateTime consentDate;
    private LocalDateTime expiryDate;
    private String legalBasis;
    private String version;
}

@Service
public class ConsentService {
    
    public boolean hasValidConsent(Long userId, String purpose) {
        ConsentRecord consent = consentRepository
            .findByUserIdAndPurposeAndStatus(userId, purpose, ConsentStatus.GRANTED);
        
        return consent != null && 
               consent.getExpiryDate().isAfter(LocalDateTime.now());
    }
    
    public void revokeConsent(Long userId, String purpose) {
        ConsentRecord consent = consentRepository
            .findByUserIdAndPurpose(userId, purpose);
        
        if (consent != null) {
            consent.setStatus(ConsentStatus.REVOKED);
            consent.setRevokeDate(LocalDateTime.now());
            consentRepository.save(consent);
            
            // 觸發資料清理
            dataCleanupService.scheduleCleanup(userId, purpose);
        }
    }
}

9.2 資料分類與標記

資料分類標準

public enum DataClassification {
    PUBLIC("Public", 0),
    INTERNAL("Internal", 1),
    CONFIDENTIAL("Confidential", 2),
    RESTRICTED("Restricted", 3);
    
    private final String level;
    private final int priority;
}

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataClassified {
    DataClassification level();
    String purpose() default "";
    boolean requiresAudit() default false;
}

@Entity
@DataClassified(level = DataClassification.CONFIDENTIAL)
public class FinancialRecord {
    
    @DataClassified(level = DataClassification.RESTRICTED, requiresAudit = true)
    private BigDecimal salary;
    
    @DataClassified(level = DataClassification.CONFIDENTIAL)
    private String accountNumber;
}

9.3 資料遮罩與匿名化

動態資料遮罩

@Component
public class DataMaskingService {
    
    public String maskEmail(String email) {
        if (email == null || email.isEmpty()) return email;
        
        String[] parts = email.split("@");
        if (parts.length != 2) return email;
        
        String username = parts[0];
        String domain = parts[1];
        
        if (username.length() <= 2) {
            return "*".repeat(username.length()) + "@" + domain;
        }
        
        return username.charAt(0) + 
               "*".repeat(username.length() - 2) + 
               username.charAt(username.length() - 1) + 
               "@" + domain;
    }
    
    public String maskCreditCard(String cardNumber) {
        if (cardNumber == null || cardNumber.length() < 4) return cardNumber;
        
        return "*".repeat(cardNumber.length() - 4) + 
               cardNumber.substring(cardNumber.length() - 4);
    }
    
    public String anonymizeData(String data, String salt) {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(salt.getBytes());
        byte[] hash = digest.digest(data.getBytes());
        return Base64.getEncoder().encodeToString(hash);
    }
}

批次資料匿名化

@Service
public class DataAnonymizationService {
    
    @Transactional
    public void anonymizeUserData(Long userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("User not found"));
        
        // 保留必要的業務識別符,匿名化敏感資料
        user.setEmail(generateAnonymousEmail());
        user.setPhoneNumber(null);
        user.setName("Anonymous User " + generateRandomId());
        user.setAddress(null);
        user.setDateOfBirth(null);
        
        // 標記為已匿名化
        user.setAnonymized(true);
        user.setAnonymizationDate(LocalDateTime.now());
        
        userRepository.save(user);
        
        // 清理相關的敏感資料
        cleanupRelatedData(userId);
    }
    
    private void cleanupRelatedData(Long userId) {
        // 清理或匿名化相關表中的資料
        auditLogRepository.deleteByUserId(userId);
        sessionRepository.deleteByUserId(userId);
        // 保留業務必要的資料,但移除個人識別資訊
    }
}

9.4 資料備份與復原

安全備份策略

#!/bin/bash
# secure-backup.sh

# 設定變數
BACKUP_DIR="/secure/backups"
DATE=$(date +%Y%m%d_%H%M%S)
DATABASE_NAME="myapp"
ENCRYPTION_KEY_FILE="/secure/keys/backup.key"

# 建立備份目錄
mkdir -p ${BACKUP_DIR}/${DATE}

# 資料庫備份
mysqldump --single-transaction --routines --triggers \
          --user="${DB_USER}" --password="${DB_PASSWORD}" \
          ${DATABASE_NAME} > ${BACKUP_DIR}/${DATE}/${DATABASE_NAME}.sql

# 檔案系統備份
tar -czf ${BACKUP_DIR}/${DATE}/files.tar.gz /app/data

# 加密備份檔案
gpg --cipher-algo AES256 --compress-algo 1 --symmetric \
    --keyring ${ENCRYPTION_KEY_FILE} \
    --output ${BACKUP_DIR}/${DATE}/${DATABASE_NAME}.sql.gpg \
    ${BACKUP_DIR}/${DATE}/${DATABASE_NAME}.sql

gpg --cipher-algo AES256 --compress-algo 1 --symmetric \
    --keyring ${ENCRYPTION_KEY_FILE} \
    --output ${BACKUP_DIR}/${DATE}/files.tar.gz.gpg \
    ${BACKUP_DIR}/${DATE}/files.tar.gz

# 刪除未加密的備份
rm ${BACKUP_DIR}/${DATE}/${DATABASE_NAME}.sql
rm ${BACKUP_DIR}/${DATE}/files.tar.gz

# 驗證備份完整性
sha256sum ${BACKUP_DIR}/${DATE}/*.gpg > ${BACKUP_DIR}/${DATE}/checksums.txt

# 清理舊備份(保留 30 天)
find ${BACKUP_DIR} -type d -mtime +30 -exec rm -rf {} \;

echo "Backup completed: ${BACKUP_DIR}/${DATE}"

10. 事件回應與復原

10.1 安全事件識別

異常行為監控

@Component
public class SecurityEventDetector {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @EventListener
    public void handleLoginAttempt(AuthenticationFailureBadCredentialsEvent event) {
        String username = event.getAuthentication().getName();
        String clientIP = getClientIP();
        
        // 記錄失敗登入
        String key = "login_failures:" + username + ":" + clientIP;
        String count = redisTemplate.opsForValue().get(key);
        
        if (count == null) {
            redisTemplate.opsForValue().set(key, "1", Duration.ofMinutes(15));
        } else {
            int failureCount = Integer.parseInt(count);
            redisTemplate.opsForValue().increment(key);
            
            if (failureCount >= 5) {
                // 觸發安全事件
                SecurityIncident incident = new SecurityIncident(
                    IncidentType.BRUTE_FORCE_ATTACK,
                    "Multiple login failures for user: " + username,
                    clientIP,
                    Severity.HIGH
                );
                
                securityIncidentService.reportIncident(incident);
                
                // 暫時鎖定帳號
                userService.lockAccount(username, Duration.ofMinutes(30));
            }
        }
    }
    
    @EventListener
    public void handlePrivilegeEscalation(AuthorizationEvent event) {
        if (event.getDecision() == AccessDecision.DENIED) {
            String username = event.getAuthentication().getName();
            String resource = event.getResource();
            
            SecurityIncident incident = new SecurityIncident(
                IncidentType.UNAUTHORIZED_ACCESS_ATTEMPT,
                String.format("User %s attempted to access %s", username, resource),
                getClientIP(),
                Severity.MEDIUM
            );
            
            securityIncidentService.reportIncident(incident);
        }
    }
}

10.2 事件回應流程

自動化事件回應

@Service
public class IncidentResponseService {
    
    @Async
    public void handleSecurityIncident(SecurityIncident incident) {
        try {
            // 1. 記錄事件
            logIncident(incident);
            
            // 2. 評估威脅等級
            ThreatLevel threatLevel = assessThreatLevel(incident);
            
            // 3. 執行自動化回應
            switch (threatLevel) {
                case CRITICAL:
                    handleCriticalThreat(incident);
                    break;
                case HIGH:
                    handleHighThreat(incident);
                    break;
                case MEDIUM:
                    handleMediumThreat(incident);
                    break;
                case LOW:
                    handleLowThreat(incident);
                    break;
            }
            
            // 4. 通知相關人員
            notifySecurityTeam(incident, threatLevel);
            
        } catch (Exception e) {
            logger.error("Error handling security incident", e);
        }
    }
    
    private void handleCriticalThreat(SecurityIncident incident) {
        // 立即隔離受影響的系統
        if (incident.getType() == IncidentType.MALWARE_DETECTED) {
            isolateAffectedSystems(incident.getAffectedSystems());
        }
        
        // 啟動緊急回應程序
        emergencyResponseService.activate();
        
        // 立即通知 CISO 和管理層
        notificationService.sendUrgentAlert(incident);
    }
    
    private void handleHighThreat(SecurityIncident incident) {
        // 增強監控
        enhanceMonitoring(incident.getSourceIP());
        
        // 暫時限制存取
        accessControlService.temporaryRestriction(
            incident.getUsername(), 
            Duration.ofHours(1)
        );
        
        // 收集額外資訊
        forensicsService.collectEvidence(incident);
    }
}

事件分類與優先順序

@Entity
public class SecurityIncident {
    @Id
    private Long id;
    
    @Enumerated(EnumType.STRING)
    private IncidentType type;
    
    @Enumerated(EnumType.STRING)
    private Severity severity;
    
    @Enumerated(EnumType.STRING)
    private IncidentStatus status;
    
    private String description;
    private String sourceIP;
    private String username;
    private LocalDateTime detectedAt;
    private LocalDateTime resolvedAt;
    
    @OneToMany(mappedBy = "incident", cascade = CascadeType.ALL)
    private List<IncidentAction> actions;
}

public enum IncidentType {
    BRUTE_FORCE_ATTACK,
    SQL_INJECTION_ATTEMPT,
    XSS_ATTEMPT,
    UNAUTHORIZED_ACCESS_ATTEMPT,
    MALWARE_DETECTED,
    DATA_BREACH,
    DDOS_ATTACK,
    PRIVILEGE_ESCALATION
}

public enum Severity {
    LOW(1), MEDIUM(2), HIGH(3), CRITICAL(4);
    
    private final int level;
}

10.3 取證與證據保全

數位取證工具

@Service
public class DigitalForensicsService {
    
    public ForensicsReport collectEvidence(SecurityIncident incident) {
        ForensicsReport report = new ForensicsReport(incident);
        
        // 1. 保全系統狀態
        report.addEvidence(captureSystemSnapshot());
        
        // 2. 收集日誌
        report.addEvidence(collectRelevantLogs(incident));
        
        // 3. 網路流量分析
        report.addEvidence(analyzeNetworkTraffic(incident.getSourceIP()));
        
        // 4. 記憶體快照
        if (incident.getSeverity() == Severity.CRITICAL) {
            report.addEvidence(captureMemoryDump());
        }
        
        // 5. 檔案完整性檢查
        report.addEvidence(checkFileIntegrity());
        
        // 計算證據鏈雜湊值
        report.generateChainOfCustody();
        
        return report;
    }
    
    private Evidence collectRelevantLogs(SecurityIncident incident) {
        LocalDateTime startTime = incident.getDetectedAt().minusHours(1);
        LocalDateTime endTime = incident.getDetectedAt().plusMinutes(30);
        
        List<LogEntry> logs = new ArrayList<>();
        
        // 應用程式日誌
        logs.addAll(applicationLogService.getLogs(startTime, endTime));
        
        // 安全日誌
        logs.addAll(securityLogService.getLogs(startTime, endTime));
        
        // 系統日誌
        logs.addAll(systemLogService.getLogs(startTime, endTime));
        
        // 篩選相關日誌
        logs = logs.stream()
            .filter(log -> isRelevantToIncident(log, incident))
            .collect(Collectors.toList());
        
        return new Evidence(EvidenceType.LOG_FILES, logs);
    }
    
    private Evidence captureSystemSnapshot() {
        SystemSnapshot snapshot = new SystemSnapshot();
        snapshot.setTimestamp(LocalDateTime.now());
        snapshot.setRunningProcesses(getRunningProcesses());
        snapshot.setNetworkConnections(getNetworkConnections());
        snapshot.setLoadedModules(getLoadedModules());
        snapshot.setSystemConfiguration(getSystemConfiguration());
        
        return new Evidence(EvidenceType.SYSTEM_SNAPSHOT, snapshot);
    }
}

10.4 災難復原計畫

復原優先順序

@Configuration
public class DisasterRecoveryConfig {
    
    @Bean
    public RecoveryPlan createRecoveryPlan() {
        return RecoveryPlan.builder()
            .priority(1, "認證系統", Duration.ofMinutes(15))
            .priority(2, "核心資料庫", Duration.ofMinutes(30))
            .priority(3, "主要應用程式", Duration.ofHours(1))
            .priority(4, "報表系統", Duration.ofHours(4))
            .priority(5, "分析工具", Duration.ofHours(8))
            .build();
    }
}

@Service
public class DisasterRecoveryService {
    
    public void executeRecoveryPlan(DisasterType disasterType) {
        RecoveryPlan plan = getRecoveryPlan(disasterType);
        
        for (RecoveryStep step : plan.getSteps()) {
            try {
                logger.info("Executing recovery step: {}", step.getDescription());
                
                step.execute();
                
                // 驗證復原狀態
                if (!step.verify()) {
                    throw new RecoveryException("Step verification failed: " + step.getDescription());
                }
                
                logger.info("Recovery step completed: {}", step.getDescription());
                
            } catch (Exception e) {
                logger.error("Recovery step failed: {}", step.getDescription(), e);
                
                // 執行回滾程序
                step.rollback();
                
                // 通知管理團隊
                notifyRecoveryFailure(step, e);
                
                break;
            }
        }
    }
    
    @Scheduled(cron = "0 0 2 * * SUN") // 每週日凌晨 2 點
    public void testRecoveryProcedures() {
        // 定期測試災難復原程序
        for (DisasterType type : DisasterType.values()) {
            try {
                runRecoveryTest(type);
            } catch (Exception e) {
                logger.error("Recovery test failed for disaster type: {}", type, e);
                alertService.sendAlert("災難復原測試失敗", e.getMessage());
            }
        }
    }
}

11. 日常開發檢查清單 (Checklist)

5.1 每日開發檢查項目

程式碼撰寫

  • 所有外部輸入都經過驗證
  • 使用參數化查詢,避免 SQL Injection
  • 敏感資料已加密或雜湊
  • 錯誤訊息不洩漏系統資訊
  • 沒有硬編碼密碼、金鑰、Token

權限控制

  • 每個 API 都有權限檢查
  • 實作最小權限原則
  • 敏感操作需要額外驗證

日誌記錄

  • 記錄重要操作(登入、權限變更)
  • 不記錄敏感資料(密碼、信用卡號)
  • 錯誤日誌包含足夠除錯資訊

5.2 Pull Request 檢查項目

安全審查

  • 新增的第三方套件無已知漏洞
  • 配置檔案不包含敏感資訊
  • 新增 API 有適當的權限控制
  • 輸入驗證覆蓋所有參數
  • 單元測試包含安全測試案例

程式碼品質

  • 通過靜態程式碼分析
  • 遵循編碼規範
  • 適當的錯誤處理
  • 充分的註解與文件

5.3 部署前檢查項目

環境設定

  • 移除除錯模式與測試帳號
  • 資料庫使用最小權限帳號
  • HTTPS 證書正確配置
  • 安全標頭設定完整
  • 防火牆規則正確設定

監控準備

  • 日誌系統正常運作
  • 安全監控告警設定
  • 備份與復原程序確認

12. 常見錯誤與反例

12.1 密碼處理錯誤

❌ 錯誤範例

// 明文儲存密碼
String password = "123456";
user.setPassword(password);

// 使用弱雜湊
String hash = DigestUtils.md5Hex(password);

✅ 正確範例

// 使用 bcrypt 雜湊
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));
user.setPassword(hashedPassword);

// 驗證密碼
boolean isValid = BCrypt.checkpw(inputPassword, user.getPassword());

12.2 SQL 查詢錯誤

❌ 錯誤範例

// SQL Injection 風險
String sql = "SELECT * FROM users WHERE id = " + userId;
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

✅ 正確範例

// 使用 PreparedStatement
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setLong(1, userId);
ResultSet rs = pstmt.executeQuery();

12.3 檔案上傳錯誤

❌ 錯誤範例

// 未檢查檔案類型
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) {
    file.transferTo(new File("/uploads/" + file.getOriginalFilename()));
    return "success";
}

✅ 正確範例

@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) {
    // 檢查檔案類型
    if (!isAllowedFileType(file)) {
        throw new IllegalArgumentException("不允許的檔案類型");
    }
    
    // 檢查檔案大小
    if (file.getSize() > MAX_FILE_SIZE) {
        throw new IllegalArgumentException("檔案過大");
    }
    
    // 生成安全檔名
    String safeFilename = generateSafeFilename(file.getOriginalFilename());
    file.transferTo(new File("/uploads/" + safeFilename));
    return "success";
}

12.4 前端 XSS 錯誤

❌ 錯誤範例(JavaScript)

// 直接插入用戶輸入
document.getElementById('content').innerHTML = userInput;

// 未過濾的 URL 重導
window.location = userProvidedUrl;

✅ 正確範例(JavaScript)

// 使用 textContent 避免 XSS
document.getElementById('content').textContent = userInput;

// 驗證 URL 後重導
if (isValidUrl(userProvidedUrl)) {
    window.location = userProvidedUrl;
}

12.5 權限控制錯誤

❌ 錯誤範例

// 僅依賴前端權限控制
@GetMapping("/admin/users")
public List<User> getUsers() {
    return userService.getAllUsers(); // 未檢查權限
}

✅ 正確範例

// 後端強制權限檢查
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> getUsers() {
    return userService.getAllUsers();
}

實務提醒

  • 永遠不要相信前端傳來的資料
  • 安全檢查應該在後端進行
  • 使用多層防禦策略

12.6 API 設計錯誤

❌ 錯誤範例

// 暴露過多資訊
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElse(null); // 暴露所有欄位
}

// 缺少權限檢查
@DeleteMapping("/api/users/{id}")
public void deleteUser(@PathVariable Long id) {
    userRepository.deleteById(id); // 任何人都可以刪除
}

// 缺少輸入驗證
@PostMapping("/api/search")
public List<User> searchUsers(@RequestParam String query) {
    return userRepository.findByNameContaining(query); // SQL Injection 風險
}

✅ 正確範例

// 資料過濾與權限控制
@GetMapping("/api/users/{id}")
@PreAuthorize("hasRole('ADMIN') or @userService.isOwner(authentication.name, #id)")
public UserDTO getUser(@PathVariable @Min(1) Long id) {
    User user = userService.findById(id);
    return userMapper.toDTO(user); // 只返回必要欄位
}

// 嚴格權限檢查
@DeleteMapping("/api/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable @Min(1) Long id) {
    if (!userService.canDelete(id)) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }
    userService.deleteUser(id);
    return ResponseEntity.noContent().build();
}

// 安全的搜尋實作
@PostMapping("/api/search")
public ResponseEntity<List<UserDTO>> searchUsers(
        @Valid @RequestBody SearchRequest request) {
    
    // 輸入驗證
    if (request.getQuery().length() < 2) {
        return ResponseEntity.badRequest().build();
    }
    
    // 使用安全的查詢方法
    List<User> users = userService.searchUsers(request.getQuery());
    return ResponseEntity.ok(userMapper.toDTOList(users));
}

12.7 配置錯誤

❌ 錯誤範例

# application.yml - 開發設定用於生產環境
spring:
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password123  # 明文密碼
  jpa:
    show-sql: true  # 生產環境顯示 SQL
    hibernate:
      ddl-auto: create-drop  # 每次重啟刪除資料

server:
  error:
    include-stacktrace: always  # 暴露錯誤堆疊
    include-message: always

management:
  endpoints:
    web:
      exposure:
        include: "*"  # 暴露所有 Actuator 端點

logging:
  level:
    org.springframework.web: DEBUG  # 過詳細的日誌

✅ 正確範例

# application-prod.yml - 生產環境配置
spring:
  profiles:
    active: prod
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 20000
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

server:
  error:
    include-stacktrace: never
    include-message: never
  port: ${SERVER_PORT:8080}

management:
  endpoints:
    enabled-by-default: false
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      enabled: true
      show-details: when-authorized
    info:
      enabled: true

logging:
  level:
    com.company.myapp: INFO
    org.springframework.security: WARN
  file:
    name: /var/log/myapp/application.log
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

security:
  jwt:
    secret: ${JWT_SECRET}
    expiration: 3600000

13. 合規性與法規要求

13.1 GDPR 合規

資料處理合法性

@Entity
public class DataProcessingRecord {
    @Id
    private Long id;
    
    private Long userId;
    private String dataCategory;
    private String processingPurpose;
    private String legalBasis; // GDPR Article 6
    private LocalDateTime processingDate;
    private LocalDateTime retentionDeadline;
    private boolean consentRequired;
    private ConsentStatus consentStatus;
}

@Service
public class GDPRComplianceService {
    
    public void processPersonalData(Long userId, String dataCategory, 
                                   String purpose, LegalBasis legalBasis) {
        
        // 檢查處理的合法性
        if (!isLegalBasisValid(legalBasis, purpose)) {
            throw new IllegalProcessingException("Invalid legal basis for purpose");
        }
        
        // 如需同意,檢查同意狀態
        if (legalBasis == LegalBasis.CONSENT) {
            ConsentRecord consent = consentService.getConsent(userId, purpose);
            if (consent == null || !consent.isValid()) {
                throw new ConsentRequiredException("Valid consent required");
            }
        }
        
        // 記錄處理活動
        DataProcessingRecord record = new DataProcessingRecord();
        record.setUserId(userId);
        record.setDataCategory(dataCategory);
        record.setProcessingPurpose(purpose);
        record.setLegalBasis(legalBasis.toString());
        record.setProcessingDate(LocalDateTime.now());
        record.setRetentionDeadline(calculateRetentionDeadline(purpose));
        
        dataProcessingRepository.save(record);
    }
    
    public void handleDataSubjectRequest(DataSubjectRequest request) {
        switch (request.getType()) {
            case ACCESS:
                handleAccessRequest(request);
                break;
            case RECTIFICATION:
                handleRectificationRequest(request);
                break;
            case ERASURE:
                handleErasureRequest(request);
                break;
            case PORTABILITY:
                handlePortabilityRequest(request);
                break;
            case RESTRICTION:
                handleRestrictionRequest(request);
                break;
        }
    }
    
    private void handleErasureRequest(DataSubjectRequest request) {
        Long userId = request.getUserId();
        
        // 檢查是否有保留義務
        if (hasLegalRetentionObligation(userId)) {
            throw new RetentionObligationException(
                "Data must be retained for legal purposes");
        }
        
        // 執行資料刪除或匿名化
        userService.anonymizeUser(userId);
        
        // 通知第三方處理者
        notifyThirdPartyProcessors(userId, request.getType());
        
        // 記錄處理結果
        recordDataSubjectRequestResponse(request, "Data erased successfully");
    }
}

13.2 個資法合規

個人資料蒐集告知

@Service
public class PrivacyNoticeService {
    
    public PrivacyNotice generatePrivacyNotice(String purpose, 
                                             List<String> dataCategories) {
        return PrivacyNotice.builder()
            .organization("公司名稱")
            .contactInfo("privacy@company.com")
            .collectionPurpose(purpose)
            .dataCategories(dataCategories)
            .retentionPeriod(getRetentionPeriod(purpose))
            .thirdPartyRecipients(getThirdPartyRecipients(purpose))
            .dataSubjectRights(getDataSubjectRights())
            .build();
    }
    
    private List<String> getDataSubjectRights() {
        return Arrays.asList(
            "查詢或請求閱覽個人資料",
            "請求製給複製本",
            "請求補充或更正個人資料",
            "請求停止蒐集、處理或利用個人資料",
            "請求刪除個人資料"
        );
    }
}

@RestController
public class PrivacyController {
    
    @PostMapping("/api/privacy/collect-consent")
    public ResponseEntity<ConsentResult> collectConsent(
            @Valid @RequestBody ConsentRequest request) {
        
        // 顯示隱私權告知
        PrivacyNotice notice = privacyNoticeService.generatePrivacyNotice(
            request.getPurpose(), request.getDataCategories());
        
        // 記錄同意
        ConsentRecord consent = new ConsentRecord();
        consent.setUserId(request.getUserId());
        consent.setPurpose(request.getPurpose());
        consent.setConsentDate(LocalDateTime.now());
        consent.setPrivacyNoticeVersion(notice.getVersion());
        consent.setStatus(ConsentStatus.GRANTED);
        
        consentService.saveConsent(consent);
        
        return ResponseEntity.ok(new ConsentResult("Consent recorded", notice));
    }
}

13.3 PCI DSS 合規

信用卡資料處理

@Service
public class PaymentSecurityService {
    
    // 使用 PCI DSS 合規的加密
    @Value("${pci.encryption.key}")
    private String encryptionKey;
    
    public String tokenizeCardNumber(String cardNumber) {
        // 驗證卡號格式
        if (!isValidCardNumber(cardNumber)) {
            throw new InvalidCardNumberException("Invalid card number format");
        }
        
        // 生成符合 PCI DSS 的 Token
        String token = generateSecureToken();
        
        // 儲存 Token 映射(使用 HSM 或安全儲存)
        tokenVaultService.storeTokenMapping(token, 
            encryptCardNumber(cardNumber));
        
        // 不保留原始卡號
        return token;
    }
    
    private String encryptCardNumber(String cardNumber) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
            
            byte[] encrypted = cipher.doFinal(cardNumber.getBytes());
            byte[] iv = cipher.getIV();
            
            // 結合 IV 和加密資料
            byte[] result = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, result, 0, iv.length);
            System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
            
            return Base64.getEncoder().encodeToString(result);
            
        } catch (Exception e) {
            throw new EncryptionException("Failed to encrypt card number", e);
        }
    }
    
    @Scheduled(fixedRate = 300000) // 5 分鐘
    public void clearMemoryBuffers() {
        // 清理記憶體中的敏感資料
        System.gc();
    }
}

// PCI DSS 需要的網路隔離
@Configuration
public class PCINetworkConfig {
    
    @Bean
    public FilterRegistrationBean<PCINetworkFilter> pciNetworkFilter() {
        FilterRegistrationBean<PCINetworkFilter> registration = 
            new FilterRegistrationBean<>();
        
        registration.setFilter(new PCINetworkFilter());
        registration.addUrlPatterns("/api/payment/*");
        registration.setOrder(1);
        
        return registration;
    }
}

public class PCINetworkFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String clientIP = getClientIP(httpRequest);
        
        // 檢查是否來自允許的 PCI 網段
        if (!isPCICompliantNetwork(clientIP)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
            return;
        }
        
        chain.doFilter(request, response);
    }
}

13.4 SOX 合規

財務資料稽核軌跡

@Entity
@Table(name = "financial_audit_log")
public class FinancialAuditLog {
    @Id
    private Long id;
    
    private String transactionId;
    private String userId;
    private String action;
    private String tableName;
    private String oldValue;
    private String newValue;
    private LocalDateTime timestamp;
    private String ipAddress;
    private String sessionId;
    
    // 防止竄改的數位簽章
    @Column(name = "digital_signature")
    private String digitalSignature;
}

@Aspect
@Component
public class SOXAuditAspect {
    
    @Around("@annotation(SOXAuditable)")
    public Object auditFinancialOperation(ProceedingJoinPoint joinPoint) 
            throws Throwable {
        
        String userId = getCurrentUserId();
        String sessionId = getCurrentSessionId();
        String operation = joinPoint.getSignature().getName();
        
        // 記錄操作前狀態
        Object[] args = joinPoint.getArgs();
        String beforeState = serializeArguments(args);
        
        try {
            Object result = joinPoint.proceed();
            
            // 記錄成功操作
            FinancialAuditLog auditLog = new FinancialAuditLog();
            auditLog.setUserId(userId);
            auditLog.setAction(operation);
            auditLog.setOldValue(beforeState);
            auditLog.setNewValue(serializeResult(result));
            auditLog.setTimestamp(LocalDateTime.now());
            auditLog.setSessionId(sessionId);
            
            // 生成數位簽章防止竄改
            auditLog.setDigitalSignature(
                digitalSignatureService.sign(auditLog.toString()));
            
            auditRepository.save(auditLog);
            
            return result;
            
        } catch (Exception e) {
            // 記錄失敗操作
            FinancialAuditLog errorLog = new FinancialAuditLog();
            errorLog.setUserId(userId);
            errorLog.setAction(operation + "_FAILED");
            errorLog.setOldValue(beforeState);
            errorLog.setNewValue("ERROR: " + e.getMessage());
            errorLog.setTimestamp(LocalDateTime.now());
            
            auditRepository.save(errorLog);
            
            throw e;
        }
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SOXAuditable {
    String description() default "";
}

// 使用範例
@Service
public class FinancialService {
    
    @SOXAuditable(description = "Update financial record")
    @Transactional
    public void updateFinancialRecord(Long recordId, 
                                    FinancialData newData) {
        
        // 額外的 SOX 控制
        if (!hasApprovalForFinancialChange(recordId, newData)) {
            throw new InsufficientApprovalException(
                "Financial changes require proper approval");
        }
        
        FinancialRecord record = financialRepository.findById(recordId)
            .orElseThrow(() -> new EntityNotFoundException("Record not found"));
        
        // 記錄變更前後的值
        FinancialRecord originalRecord = record.clone();
        record.updateFrom(newData);
        
        financialRepository.save(record);
        
        // 通知相關人員
        notificationService.notifyFinancialChange(originalRecord, record);
    }
}

14. 延伸資源

14.1 官方安全指引

OWASP 資源

政府與標準

14.2 程式語言特定資源

Java

Python

JavaScript

14.3 安全工具

靜態程式碼分析

  • SonarQube
  • Checkmarx
  • Veracode

相依性掃描

  • OWASP Dependency Check
  • Snyk
  • GitHub Dependabot

動態測試

  • OWASP ZAP
  • Burp Suite
  • SQLMap

14.4 學習資源

線上課程

社群資源

14.5 公司內部資源

政策文件

  • 資訊安全政策
  • 資料分類與處理準則
  • 事件回應程序

工具與平台

  • 公司 SIEM 系統使用指南
  • 安全代碼審查流程
  • 漏洞管理平台

聯絡窗口

  • 資訊安全部門:security@company.com
  • 資安事件回報:incident@company.com
  • 安全問題諮詢:infosec-help@company.com

結語

安全程式碼撰寫是每位開發者的責任,也是保護公司與客戶的第一道防線。本指引提供了基本的安全開發原則與實務範例,但安全威脅持續演進,開發者需要保持學習與警覺。

重要提醒

  1. 安全是過程,不是結果 - 需要持續改進與更新
  2. 多層防禦 - 不要依賴單一安全措施
  3. 及早發現 - 設計階段考慮安全比事後修補更有效
  4. 團隊合作 - 安全需要整個團隊的共同努力

如有安全相關問題,請隨時聯絡資訊安全部門或參考延伸資源進行學習。


文件版本:v2.0
最後更新:2025年8月29日
維護者:資訊安全部門