一行代碼引來的安全漏洞就讓咱們丟失了整個服務器的控制權

以前在某廠的某次項目開發中,項目組同窗設計和實現了一個「引覺得傲」,額,有點擴張,不過自認爲還說得過去的 feature,結果臨上線前被啪啪打臉,由於實現過程當中由於一行代碼(沒有標題黨,真的是一行代碼)帶來的安全漏洞讓咱們丟失了整個服務器控制權(測試環境)。多虧了上線以前有公司安全團隊的人會對代碼進行掃描,才讓這個漏洞被扼殺在搖籃裏。html

下面咱們就一塊兒來看看這個事故,啊,不對,是故事。前端

背景說明

咱們的項目是一個面向全球用戶的 Web 項目,用 SpringBoot 開發。在項目開發過程當中,離不開各類異常信息的處理,好比表單提交參數不符合預期,業務邏輯的處理時離不開各類異常信息(例如網絡抖動等)的處理。因而利用 SpringBoot 各類現成的組件支持,設計了一個統一的異常信息處理組件,統一管理各類業務流程中可能出現的錯誤碼和錯誤信息,經過國際化的資源配置文件進行統一輸出給用戶。java

統一錯誤信息配置管理

咱們的用戶遍及全球,爲了給各個國家用戶比較好的體驗會進行不一樣的翻譯。具體而言,實現的效果以下,爲了方便理解,以「找回登陸密碼」這樣一個業務場景來進行闡述說明。程序員

假設找回密碼時,須要用戶輸入手機或者郵箱驗證碼,假設這個時候用戶輸入的驗證碼經過後臺數據庫(多是Redis)對比發現已通過期。在業務代碼中,只須要簡單的 throw new ErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED) 便可。具體而言,針對不一樣國家地區不一樣的語言看到的效果不同:web

  • 中文用戶看到的提示就是「您輸入的驗證碼已過時,請從新獲取」;
  • 歐美用戶看到的效果是「The verification code you input is expired, ...」;
  • 德國用戶看到的是:「Der von Ihnen eingegebene Verifizierungscode ist abgelaufen, bitte wiederholen」 。(我瞎找的翻譯,不必定準)
  • ……

統一錯誤信息配置管理代碼實現

關鍵信息其實就在於一個 GlobalExceptionHandler,對全部Controller 入口進行 AOP 攔截,根據不一樣的錯誤信息,獲取相應資源文件配置的 key,並從語言資源文件中讀取不一樣國家的錯誤翻譯信息。算法

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, BadRequestException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message));
    }
    
    @ExceptionHandler(ErrorCodeException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, ErrorCodeException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.OK).body(Response.error(e.getCode(), i18message));
    }
}
複製代碼

不一樣語言的資源文件示例

private String getI18nMessage(String key, HttpServletRequest request) {
   try {
       return messageSource.getMessage(key, null, LanguaggeUtils.currentLocale(request));
   } catch (Exception e) {
       // log
       return key;
   }
}
複製代碼

詳細代碼實現能夠參考本人以前寫的這篇文章一文教你實現 SpringBoot 中的自定義 Validator 和錯誤信息國際化配置,上面有附完整的代碼實現。spring

基於註解的表單校驗(含自定義註解)

還有一種常見的業務場景就是後端接口須要對用戶提交的表單進行校驗。以「註冊用戶」這樣的場景舉例說明, 註冊用戶時,每每會提交暱稱,性別,郵箱等信息進行註冊,簡單起見,就以這 3 個屬性爲例。shell

定義的表單以下:數據庫

public class UserRegForm {
	private String nickname;
	private String gender;
	private String email;
}
複製代碼

對於表單的約束,咱們有:編程

  • 暱稱字段:「nickname」 必填,長度必須是 6 到 20 位;
  • 性別字段:「gender」 可選,若是填了,就必須是「Male/Female/Other/」中的一種。說啥,除了男女還有其餘?對,是的。畢竟全球用戶嘛,你去看看非死不可,還有更多。
  • 郵箱: 「email」,必填,必須知足郵箱格式。

對於以上約束,咱們只須要在對應的字段上添加以下註解便可。

public class UserRegForm {
	@Length(min = 6, max = 20, message = "validate.userRegForm.nickname")
	private String nickname;

	@Gender(message="validate.userRegForm.gender")
	private String gender;

	@NotNull
	@Email(message="validate.userRegForm.email")
	private String email;
}
複製代碼

而後在各個語言資源文件中配置好相應的錯誤信息提示便可。其中, @Gender 就是一個自定義的註解。

基於含自定義註解的表單校驗關鍵代碼

自定義註解的實現主要的其實就是一個自定義註解的定義以及一個校驗邏輯。 例如定義一個自定義註解 CustomParam

@Documented
@Constraint(validatedBy = CustomValidator.class)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomParam {
    String message() default "name.tanglei.www.validator.CustomArray.defaultMessage";

    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default { };

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @interface List {
        CustomParam[] value();
    }
}
複製代碼

校驗邏輯的實現 CustomValidator

public class CustomValidator implements ConstraintValidator<CustomParam, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s || s.isEmpty()) {
            return true;
        }
        if (s.equals("tanglei")) {
            return true;
        } else {
            error(constraintValidatorContext, "Invalid params: " + s);
            return false;
        }
    }

    @Override
    public void initialize(CustomParam constraintAnnotation) {
    }

    private static void error(ConstraintValidatorContext context, String message) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}
複製代碼

上面例子只爲了闡述說明問題,其中校驗邏輯沒有實際意義,這樣,若是輸入參數不知足條件,就會明確提示用戶輸入的哪一個參數不知足條件。例如輸入參數 xx,則會直接提示:Invalid params: xx

這個跟第一部分的處理方式相似,由於現有的 validator 組件實現中,若是違反相應的約束也是一種拋異常的方式實現的,所以只須要在上述的 GlobalExceptionHandler中添加相應的異常信息便可,這裏就不詳述了。 這不是本文的重點,這裏就不詳細闡述了。 詳細代碼實現能夠參考本人以前寫的這篇文章一文教你實現 SpringBoot 中的自定義 Validator 和錯誤信息國際化配置,上面有附完整的代碼實現。

場景重現

一切都顯得很完美,直到上線前代碼提交至安全團隊掃描,就被「啪啪打臉」,掃描報告反饋了一個嚴重的安全漏洞。而這個安全漏洞,屬於很高危的遠程代碼執行漏洞。

用前文提到的自定義 Validator,輸入的參數用: 「1+1=${1+1}」,看看效果:

太 TM 神奇了,竟然幫我運算出來了,返回 "message": "Invalid params: 1+1=2"

問題就出如今實現自定義註解進行校驗的這行代碼(以下圖所示):

其實,最開始的時候,這裏直接返回了「Invalid params」,當初爲了更好的用戶體驗,要明確告訴用戶哪一個參數沒有經過校驗,所以在輸出的提示上加上了用戶輸入的字段,也就是上面的"Invalid params: " + s,沒想到,這闖了大禍了(回過頭來想,感受這裏不必這麼詳細啊,由於前端已經有相應的校驗了,正常狀況下回攔住,針對不守規矩的用很是規手段來的接口請求,直接返回校驗不經過就好了,畢竟不是對外提供的 OpenAPI 服務)。

仔細看,這個方法其實是 ConstraintValidatorContext這個接口中聲明的,看方法名字其實能知道輸入參數是一個字符串模板,內部會進行解析替換的(這其實也符合「見名知意」的良好編程習慣)。(教訓:你們應該把握好本身寫的每一行代碼背後實際在作什麼。)

/* ...... * @param messageTemplate new un-interpolated constraint message * @return returns a constraint violation builder */
ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
複製代碼

這個 case,源碼調試進去以後,就能跟蹤到執行翻譯階段,在以下方法中: org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage

再日後,就是表達式求值了。

覺得就這樣就完了嗎?

剛開始感受,能幫忙算簡單的運算規則也就完了吧,你還能把我怎麼樣?其實這個至關於暴露了一個入口,支持用戶輸入任意 EL 表達式進行執行。網上經過關鍵字 「SpEL表達式注入漏洞」 找找,就能發現事情並無想象中那麼簡單。

咱們構造恰當的 EL 表達式(注意各類轉義,下文的輸入參數相對比較明顯在作什麼了,實際上還有更多黑科技,好比各類二進制轉義編碼啊等等),就能直接執行輸入代碼,例如:能夠直接執行命令,「ls -al」, 返回了一個 UNIXProcess 實例,命令已經被執行過了。

好比,咱們執行個打開計算器的命令,搞個計算器玩玩~

我錄製了一個動圖,來個演示可能更生動一些。

這還得了嗎?這至關於提供了一個 webshell 的功能呀,你看想運行啥命令就能運行啥命令,例如 ping 本人博客地址(ping www.tanglei.name),下面動圖演示一下整個過程(從運行 ping 到 kill ping)。

我錄製了一個視頻,點擊這裏能夠訪問。

豈不是直接建立一個用戶,而後遠程登陸就能夠了。後果很嚴重啊,別人想幹嗎就幹嗎了。

咱們跟蹤下對應的代碼,看看內部實現,就會「恍然大悟」了。

經驗教訓

幸好這個漏洞被扼殺在搖籃裏,不然後果還真的挺嚴重的。經過這個案例,咱們有啥經驗和教訓呢?那就是做爲程序員,咱們要對每一行代碼都保持「敬畏」之心。也許就是由於你的不經意的一行代碼就帶來了嚴重的安全漏洞,要是不當心被壞人利用,輕則……重則……(本身想象吧)

此外,咱們也應該看到,程序員須要對常見的安全漏洞(例如XSS/CSRF/SQL注入等等)有所瞭解,而且要有足夠的安全意識(其實有時候研究一些安全問題還挺好玩的,好比這篇《RSA算法及一種"旁門左道"的攻擊方式》就比較有趣)。例如:

  • 用戶權限分離:運行程序的用戶不該該用 root,例如新建一個「web」或者「www」之類的用戶,並設置該用戶的權限,好比不能有可執行 xx 的權限之類的。本文 case,若是權限進行了分離(遵循最小權限原則),應該也不會這麼嚴重。(本文就恰好是由於是測試環境,因此沒有強制實施)
  • 任什麼時候候都不要相信用戶的輸入,必須對用戶輸入的進行校驗和過濾,又特別是針對公網上的應用。
  • 敏感信息加密保存。退一萬步講,假設攻擊者攻入了你的服務器,若是這個時候,你的數據庫帳戶信息等配置都直接明文保存在服務器中。那數據庫也被脫走了。

若是可能的話,須要對開發者的代碼進行漏洞掃描。一些常見的安全漏洞如今應該是有現成的工具支持的。另外,讓專業的人作專業的事情,例如要有安全團隊,可能你會說大家公司沒有不也活的好好的,哈哈,只不過可能尚未被壞人盯上而已,壞人也會考慮到他們的成本和預期收益的,固然這就更加對咱們開發者提升了要求。一些敏感權限盡可能控制在少部分人手中,配合相應的流程來支撐(不得不說,大公司繁瑣的流程仍是有必定道理的)。

畢竟我不是專業研究Web安全的,以上說得可能也不必定對,若是你有不一樣意見或者更好的建議歡迎留言參與討論。

這篇文章從寫代碼作實驗,到錄屏作視頻動圖等等耗時還蠻久的(好幾個週末的時間呢),原創真心不易,但願你能幫我個小忙唄,若是本文內容你以爲有所啓發,有所收穫,請幫忙點個「在看」唄,或者轉發分享讓更多的小夥伴看到。

精彩推薦

文章首發於本人微信公衆號(ID:tangleithu),請感興趣的同窗關注個人微信公衆號,及時獲取技術乾貨。

相關文章
相關標籤/搜索