:從SQL注入到越權防護)
Spring Boot 項目跑了大半年業(yè)務倒是穩(wěn)得很直到某天安全掃描報告甩到眼前——SQL注入、敏感信息明文傳輸、越權訪問一個個紅字標得刺眼。說是修復漏洞其實背后牽扯出的是一整套安全檢查項接口設計、鑒權模型、依賴版本、甚至運維部署習慣都得重新過一遍。這篇博文把我在Spring Boot后端開發(fā)里做安全漏洞修復的全過程梳理出來從漏洞分類、原理拆解到具體修法、碰到過的翻車現(xiàn)場一次性講透。不管你是剛接手老項目的新手還是要給現(xiàn)有系統(tǒng)做安全加固的負責人照著這套思路走一遍心里基本就有底了。1. 安全漏洞修復的整體思路先盤點再動手1.1 后端項目里最常見的五類安全漏洞我在實際工作中接觸過的Spring Boot項目不管是企業(yè)內部管理系統(tǒng)還是對外開放的API服務安全漏洞基本集中在下面幾類參數(shù)注入類SQL注入、命令注入、SpEL注入、跨站腳本XSS、請求偽造與跨域問題CSRF、CORS配置不當、越權訪問水平越權、垂直越權、敏感信息泄露硬編碼密鑰、明文傳輸、日志泄漏。這幾類漏洞占了日常修復工作量的八成以上。其中SQL注入是老牌經(jīng)典尤其常見于使用MyBatis的項目里。很多團隊習慣用${}做字符串拼接一旦參數(shù)被外部控制后果直接就是拖庫。XSS則容易被當成前端的事但后端如果不做輸出編碼和過濾攻擊者照樣可以通過接口把惡意腳本喂給其他用戶。越權問題更隱蔽接口明明鑒權了但數(shù)據(jù)歸屬校驗沒做A用戶傳個B用戶的訂單ID就能查走別人的數(shù)據(jù)。1.2 修復優(yōu)先級怎么排風險和成本怎么權衡安全漏洞修起來往往牽一發(fā)動全身所以不能拿到報告就亂改。我的習慣是先按兩個維度做評估危害程度和修復成本。危害程度看的是攻擊者利用這個漏洞能拿到什么——是能拖庫、能控制服務器還是只能偷看點不太重要的數(shù)據(jù)。修復成本看的是要改多少代碼、會不會影響現(xiàn)有業(yè)務邏輯、需不需要上線窗口。按這個評估邏輯SQL注入和硬編碼密鑰這類問題需要立即處理因為攻擊路徑清晰、自動化工具一打就中。越權問題視業(yè)務重要性決定優(yōu)先級涉及訂單、支付、用戶隱私數(shù)據(jù)的接口必須優(yōu)先修。XSS和CSRF則根據(jù)系統(tǒng)使用場景靈活安排如果是內部后臺系統(tǒng)緊迫性可以適當降低如果是面向C端的公開站點就得在最短時間內修復上線。我建議任何團隊都先把漏洞清單拉出來逐個打標分類再排修復計劃。不要一股腦地改代碼更不要抱著反正沒被攻擊就不用管的心態(tài)。安全這件事永遠是亡羊補牢的成本遠高于未雨綢繆。2. 核心漏洞修復實操從注入到越權2.1 SQL注入MyBatis場景下最容易翻車的三個點MyBatis項目里的SQL注入絕大多數(shù)不是出在大段XML映射文件上而是出在三個不起眼的地方。第一個是動態(tài)排序字段XML里寫ORDER BY ${sortField}前后端把排序字段名當參數(shù)傳進來直接拼進SQL。第二個是模糊查詢的拼接寫法有些老代碼會寫WHERE name LIKE %${keyword}%這屬于最粗暴的注入點。第三個是in語句和動態(tài)表名IN (${ids})看著方便一旦ids里有惡意內容就直接翻車。MyBatis的#{}為什么安全因為它底層走的是PreparedStatement的占位符機制參數(shù)值由驅動轉義處理不參與SQL語句結構組裝。而${}是純字符串替換參數(shù)內容原樣拼進SQL語句等于把語法結構控制權交給了調用方。很多剛入門的同學不清楚這個區(qū)別反正能用就一直用${}最后掃描報告一片紅。修起來也不復雜。排序字段和白名單映射綁定前端傳什么先經(jīng)過一層字典轉換匹配不到就返回默認值。模糊查詢統(tǒng)一改#{}或者用concat(%, #{keyword}, %)。in語句改成遍歷生成占位符比如ListLong ids Arrays.asList(1L, 2L, 3L); StringBuilder placeholders new StringBuilder(); for (int i 0; i ids.size(); i) { placeholders.append(#{idList[).append(i).append(]}); if (i ids.size() - 1) { placeholders.append(,); } } String sql SELECT * FROM orders WHERE id IN ( placeholders );這里占位符的編號是動態(tài)拼出來的但值本身全部走PreparedStatement攻擊者無論傳什么內容都只會被當參數(shù)處理。動態(tài)表名建議直接做白名單枚舉從業(yè)務層面限制候選值而不是把用戶輸入拼進表名。2.2 XSS防護后端不能只靠前端過濾XSS在我接觸的項目里有個明顯的認知誤區(qū)。很多后端同學認為前端框架Vue、React自帶轉義就夠了后端只要管好接口數(shù)據(jù)。但現(xiàn)實是微信內置瀏覽器的老內核、部分App的WebView、甚至前端工程師自己用v-html注入的地方都可能把后端返回的字符串當成HTML直接渲染。后端不做輸出編碼等于把所有防御壓在一條不可控的線上。后端的XSS修復要從兩個維度做。第一是輸入側過濾對用戶提交的內容做白名單字符校驗把script、javascript:、data:text/html這些危險模式直接攔掉。第二是輸出側編碼接口返回非富文本字段時把HTML敏感字符轉義成實體變lt;。Spring Boot項目里可以在全局JSON序列化層配置HtmlMapper或者給字段加JsonSerialize注解統(tǒng)一處理。富文本內容比較特殊直接轉義會把正常排版破壞掉。我的做法是引入Jsoup做白名單清洗只保留p、span、img、a這類安全標簽并且強制校驗href屬性必須以http或https開頭。還可以在響應頭里加X-Content-Type-Options: nosniff和Content-Security-Policy雙保險。注意一點攻擊者經(jīng)常利用Unicode全角字符、HTML數(shù)字實體做變種繞過所以過濾規(guī)則一定要覆蓋解碼后的內容。2.3 CSRF與請求偽造接口冪等和來源校驗CSRF的修復要看系統(tǒng)的交互場景。傳統(tǒng)后端渲染頁面的項目表單提交天然需要CSRF Token機制來防跨站請求偽造。前后端分離的項目反而容易翻車在CORS配置上——很多團隊為了開發(fā)方便直接allowedOrigins(*)加allowCredentials(true)這等于把站點的所有接口暴露給任意第三方網(wǎng)頁調用。Spring Boot 3.x里使用Spring Security 6.x配置方式已經(jīng)和舊版不一樣了。CSRF防護默認是開啟的但在無狀態(tài)Token鑒權的API服務里通常要顯式關閉否則前端每次請求都得帶Token反而容易被繞過。我建議的做法是內部后臺管理系統(tǒng)開啟CSRF防護使用CookieCsrfTokenRepository并配合前端讀取X-XSRF-TOKEN頭對外開放的純API服務則關掉CSRF但必須在跨域配置上做嚴格限制。Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers(/api/public/**)) .cors(cors - cors.configurationSource(corsConfigurationSource())) .build(); }跨域這塊生產(chǎn)環(huán)境嚴格設置允許的來源域名列表不要用通配符。還要注意HTTP方法和自定義頭部的校驗比如限制allowedMethods只開放實際用到的GET、POST、PUT、DELETE多余的OPTIONS預檢請求行為會影響安全策略但也不能粗暴禁止。2.4 越權漏洞數(shù)據(jù)歸屬校驗比接口鑒權更關鍵越權分兩種。水平越權是同級用戶之間的越限訪問A用戶通過改ID查B用戶數(shù)據(jù)垂直越權是低權限用戶調用高權限接口比如普通員工后臺給自己加個管理員角色。Spring Boot項目里Spring Security做得再完善解決的也只是你有沒有登錄、角色是什么的問題而你能訪問誰的資源必須靠業(yè)務層的對象歸屬校驗。水平越權的典型修復方案是引入數(shù)據(jù)權限維度。查詢訂單詳情時不僅校驗登錄狀態(tài)還要校驗訂單的userId是否等于當前登錄用戶的ID。常見的做法是在BaseEntity里設計tenantId或userId字段所有查詢SQL強制追加歸屬條件。這里有一個實戰(zhàn)細節(jié)MyBatis的攔截器可以在執(zhí)行前自動拼接數(shù)據(jù)權限SQL避免每個Mapper人工改一遍。攔截器里解析DataScope注解動態(tài)注入當前用戶的權限范圍條件既省事又不會漏改。垂直越權主要靠服務端接口的權限注解兜底。Spring Security配合PreAuthorize可以精確到方法的角色控制比如PreAuthorize(hasRole(ADMIN)) public void deleteUser(Long userId) { // 僅管理員可調用 }但注解僅僅是第一道門我見過太多因為配置漏了EnableMethodSecurity導致注解失效的情況。更保險的做法是做一個權限切面掃描接口路徑和HTTP方法匹配當前用戶角色是否在允許列表里雙保險才不容易被繞過。另外接口返回列表數(shù)據(jù)時也要做越權過濾不能只在單條查詢上做校驗否則攻擊者用遍歷方式批量撈數(shù)據(jù)照樣出事。3. 工程化防護配置靠框架不如靠規(guī)范3.1 Spring Security集成與配置要點Spring Boot項目接Spring Security并不難難的是配置方法和版本適配。Spring Boot 3.x的Security配置已經(jīng)從WebSecurityConfigurerAdapter改成基于SecurityFilterChain的Lambda風格寫法很多網(wǎng)上舊教程還在用WebSecurityConfigurerAdapter直接編譯都過不了。新寫法核心是定義一個SecurityFilterChainBean在Lambda里配置各種規(guī)則。配置過程中最容易踩的坑有三個。第一是靜態(tài)資源放行配置比如Swagger UI、上傳文件目錄處理不當會出現(xiàn)接口能訪問但頁面被攔截的詭異現(xiàn)象。第二是登錄接口的放行路徑和Token生成時機沒對齊導致登錄成功后拿不到Token。第三是過濾器順序問題自己加的自定義Filter如果注冊在Spring Security過濾器鏈之前就繞過了認證邏輯等于給攻擊者開了一扇門。我實際項目里的做法認證走LoginFilter解析JWT Token校驗通過后把用戶信息塞進SecurityContext。同時配置ExceptionTranslationFilter的統(tǒng)一異常處理保證Token過期、簽名錯誤、權限不足分別返回401和403。還要注意一處細節(jié)——Spring Security的默認登錄頁和默認用戶user 隨機密碼只在未配置時生效一旦你自定義了UserDetailsService默認配置就失效了千萬別以為集成完了就能直接用。3.2 敏感信息保護密鑰、數(shù)據(jù)庫密碼和日志泄漏我見過不少代碼把數(shù)據(jù)庫密碼、Redis密碼、第三方接口密鑰直接寫在application.yml里還堂而皇之地提交到Git倉庫。哪怕倉庫是私有的只要員工離職、代碼備份外泄這些敏感信息就全暴露了。修復這個問題不是簡單把明文改成環(huán)境變量就行而是要做一套配置管理規(guī)范?;诔R妼嵺`我推薦組合方案本地開發(fā)用.env文件加spring.profiles.activedev區(qū)分環(huán)境生產(chǎn)環(huán)境密鑰通過K8s Secret或云廠商的密鑰管理服務注入Spring Boot側用Value讀取環(huán)境變量。需要加密時可以采用Jasypt對配置文件中的敏感字段做加密處理運行環(huán)境設置解密密鑰。這里有個教訓Jasypt加密后的密文在日志里是不能打印的否則等于白加密。日志泄漏是容易被忽略的盲區(qū)。用戶手機號、身份證號、銀行卡號一旦打進日志安全掃描就抓個正著。修復方法包括統(tǒng)一Logback配置里增加脫敏規(guī)則對mobile、idCard、password等字段做掩碼輸出接口返回值用JsonIgnore或JsonProperty(access WRITE_ONLY)控制敏感字段不回傳全局異常處理時不要把SQL異常堆棧直接返回給前端。3.3 依賴漏洞排查版本升級的正確姿勢Spring Boot的依賴數(shù)量龐大任何一個間接依賴存在已知漏洞都會波及整個項目。掃描報告里經(jīng)常出現(xiàn)的CVE編號比如Log4j2的遠程代碼執(zhí)行、Spring框架的某些序列化漏洞往往靠升級版本就能解掉。Maven項目可以用OWASP Dependency-Check插件做依賴漏洞掃描在pom.xml里配置好集成到CI流水線里每次構建自動跑。plugin groupIdorg.owasp/groupId artifactIddependency-check-maven/artifactId version8.4.3/version executions execution goals goalcheck/goal /goals /execution /executions /plugin執(zhí)行mvn verify時如果掃描到高危漏洞構建會直接失敗報告會生成在當前模塊的target目錄。這種方式能強制團隊在合入代碼前就解決漏洞而不是等安全報告下來再補救。版本升級不能無腦升到最新版要注意Spring Boot 2.3.x到2.6.x再到Spring Boot 3.x中間涉及javax包名切換為jakarta、Spring Security配置廢棄等重大變更。升級前務必看官方遷移指南我經(jīng)歷過一次大版本升級因為忽略了javax.servlet改成jakarta.servlet整個項目編譯都過不了。4. 常見問題與排查技巧實錄4.1 修復漏洞引發(fā)的經(jīng)典翻車現(xiàn)場修代碼不可怕怕的是修復漏洞把功能修掛了。說起翻車現(xiàn)場我第一個想到的就是在系統(tǒng)里引入Spring Security后前端所有請求突然返回401。排查半天發(fā)現(xiàn)是前端請求沒帶Token而所有接口都被攔截器攔了。白名單里加/api/v1/login和/api/v1/refresh后發(fā)現(xiàn)還是不行最后定位到是自定義Filter里對Authorization頭的解析邏輯跟Security的過濾器鏈順序沖突了。調整Filter注冊順序后問題解決。第二個經(jīng)典翻車是SQL注入修復改成#{}后動態(tài)in查詢直接報語法錯誤。原因是我在MyBatis的XML里寫了IN (#{ids})傳過來的是逗號拼接的字符串結果整個in子句的內容被當成一個完整的參數(shù)值了。正確姿勢還是上面說的遍歷生成占位符或者用foreach標簽處理集合參數(shù)。第三個很容易踩坑的是越權修復加數(shù)據(jù)權限條件后后臺管理端的超級管理員查不到任何數(shù)據(jù)。因為攔截器里默認附加了userId 當前用戶的條件但管理員查看所有訂單是正常功能被強制加白反倒把業(yè)務邏輯打破了。正確的做法是攔截器里區(qū)分角色超管不加數(shù)據(jù)權限條件普通用戶才加歸屬過濾。4.2 一套可落地的漏洞自查清單每次安全修復完畢我都會照著自查清單過一遍防止漏項和回歸。清單僅供參考實際使用可根據(jù)團隊技術棧和業(yè)務特點調整檢查項具體檢查點驗證方式SQL注入所有SQL是否使用#{}而非${}動態(tài)排序是否白名單化代碼審計 手工注入嘗試XSS用戶輸入是否過濾接口輸出是否轉義富文本是否白名單輸入script字符串驗證回顯CSRF/CORS跨域來源是否嚴格配置CSRF Token是否有效瀏覽器控制臺模擬跨域請求越權訪問數(shù)據(jù)歸屬ID是否校驗角色權限注解是否生效用兩個賬號互相訪問對方資源敏感信息密鑰是否環(huán)境變量化日志是否脫敏密碼是否加密存儲掃描日志和配置文件依賴漏洞是否跑過Dependency-Check存在高危CVE是否升級Maven插件掃描結果另外還要檢查幾件事上傳文件類型是否做校驗防止上傳惡意JSP或木馬文件、接口是否做限流防刷防止暴力破解、HTTPS證書是否過期明文傳輸?shù)扔诼惚?。這些雖然不直接歸在漏洞類別里但安全掃描同樣會盯上。4.3 排查工具和輔助手段的取舍工具不是越多越好關鍵是能融入開發(fā)流程。SonarQube做靜態(tài)代碼掃描能抓出SQL注入、XSS等常見代碼模式適合集成到CI里每次提交自動跑。SpotBugs補充檢查字節(jié)碼層面的一些問題比如無效的空值判斷、反射調用漏洞。運行時的安全監(jiān)控我建議用Spring Boot Admin輔助觀察端點健康狀態(tài)它本身不直接防攻擊但可以發(fā)現(xiàn)異常請求模式——比如同一IP高頻訪問敏感接口這時候就該考慮加請求頻率限制Rate Limiting了。排查時有個容易忽略的點安全日志必須單獨落盤保存不能和應用日志混在一起。安全日志要記錄登錄失敗嘗試、越權訪問攔截、Token校驗失敗等關鍵事件方便事后回溯攻擊路徑。日志內容注意別把請求參數(shù)里的密碼和Token打進去否則日志文件泄露等于二次出事。5. 寫在最后安全修復沒有一次到位我始終覺得安全漏洞修復不是做完一次掃描就結束的工作更像是在持續(xù)維護的安全衛(wèi)生習慣。但這篇博文的核心是希望你能抓住幾個關鍵動作把SQL注入的寫法規(guī)范卡死、把越權校驗的規(guī)則補齊、把敏感信息的存儲方式升級、把依賴漏洞的掃描掛在CI上。只要這四件事落地項目被常見漏洞打穿的概率已經(jīng)降了九成。最后再分享一個小技巧每次修復完漏洞把修復方案、繞過嘗試方式、回歸測試結果記錄成文檔等積累多了一份你就能提煉出團隊自己的安全編碼規(guī)范。規(guī)范這種東西正式場合喊再多遍都沒用從真實漏洞里總結出來的才是團隊真正會去遵守的底線。