安全程式碼指引
目錄
- 文件目的
- 通用安全開發原則
- 程式語言安全指引
- 3.1 Java / Spring Boot
- 3.2 Python
- 3.3 JavaScript / TypeScript / Vue3
- 3.4 資料庫安全
- OWASP Top 10 對應對策
- 4.1 A01:2021 – Broken Access Control (存取控制失效)
- 4.2 A02:2021 – Cryptographic Failures (加密失效)
- 4.3 A03:2021 – Injection (注入攻擊)
- 4.4 A04:2021 – Insecure Design (不安全設計)
- 4.5 A05:2021 – Security Misconfiguration (安全設定錯誤)
- 4.6 A06:2021 – Vulnerable Components (易受攻擊元件)
- 4.7 A07:2021 – Identification and Authentication Failures (識別與驗證失效)
- 4.8 A08:2021 – Software and Data Integrity Failures (軟體與資料完整性失效)
- 4.9 A09:2021 – Security Logging & Monitoring Failures (安全日誌與監控失效)
- 4.10 A10:2021 – Server-Side Request Forgery (SSRF)
- API 安全設計
- 5.1 RESTful API 安全
- 5.2 GraphQL 安全
- 5.3 API 版本控制與向後相容
- 5.4 API 速率限制與節流
- 容器化與雲端安全
- 6.1 Docker 安全
- 6.2 Kubernetes 安全
- 6.3 雲端服務安全
- CI/CD 安全
- 安全測試與驗證
- 8.1 靜態應用程式安全測試 (SAST)
- 8.2 動態應用程式安全測試 (DAST)
- 8.3 互動式應用程式安全測試 (IAST)
- 8.4 滲透測試
- 資料保護與隱私
- 事件回應與復原
- 日常開發檢查清單 (Checklist)
- 11.1 每日開發檢查項目
- 11.2 Pull Request 檢查項目
- 11.3 部署前檢查項目
- 常見錯誤與反例
- 合規性與法規要求
- 13.1 GDPR 合規
- 13.2 個資法合規
- 13.3 PCI DSS 合規
- 13.4 SOX 合規
- 延伸資源
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: false4.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-security6.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: 54326.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-verified7.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/replicas7.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.json8. 安全測試與驗證
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.jar8.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: 360000013. 合規性與法規要求
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 學習資源
線上課程:
- OWASP WebGoat - 實戰練習
- PortSwigger Web Security Academy - 免費課程
- Secure Code Warrior - 程式碼安全訓練
社群資源:
- OWASP Local Chapter - 在地社群
- Bugcrowd University - 漏洞賞金獵人資源
14.5 公司內部資源
政策文件:
- 資訊安全政策
- 資料分類與處理準則
- 事件回應程序
工具與平台:
- 公司 SIEM 系統使用指南
- 安全代碼審查流程
- 漏洞管理平台
聯絡窗口:
- 資訊安全部門:security@company.com
- 資安事件回報:incident@company.com
- 安全問題諮詢:infosec-help@company.com
結語
安全程式碼撰寫是每位開發者的責任,也是保護公司與客戶的第一道防線。本指引提供了基本的安全開發原則與實務範例,但安全威脅持續演進,開發者需要保持學習與警覺。
重要提醒:
- 安全是過程,不是結果 - 需要持續改進與更新
- 多層防禦 - 不要依賴單一安全措施
- 及早發現 - 設計階段考慮安全比事後修補更有效
- 團隊合作 - 安全需要整個團隊的共同努力
如有安全相關問題,請隨時聯絡資訊安全部門或參考延伸資源進行學習。
文件版本:v2.0
最後更新:2025年8月29日
維護者:資訊安全部門