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