1. 程式人生 > >代碼在線編譯器(下)- 用戶代碼安全檢測

代碼在線編譯器(下)- 用戶代碼安全檢測

單獨 app form 而是 its import sad 內存 代碼demo

此文已由作者姚太行授權網易雲社區發布。

歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。

前文連接

案例的介紹已在前文中給出,本文中對相關部分將不再敘述。為更好地閱讀本文,需要簡單了解背景,建議可以大致瀏覽下前文:

代碼在線編譯器(上)- 編輯及編譯

安全檢測

在線編譯器中的安全檢測,目的是確定用戶代碼是否能夠安全的運行,且不對運行環境產生危害。仍以一般場景和特殊場景(前文有說明)舉例區分:

  • 一般場景:用戶代碼僅依賴原生庫,運行環境選擇沙箱情況下,沙箱間相互獨立,用戶代碼導致的環境損害只會作用於單一沙箱,不會影響到其他沙箱及底層系統的正常使用。

  • 特殊場景:用戶代碼依賴平臺提供API,運行環境無法使用獨立的沙箱,用戶代碼不良操作可能會引起整個運行環境的異常,從而導致其他用戶代碼運行失敗或服務器崩潰等情況。

所以,在特殊場景下,如何對用戶代碼是否符合安全要求作出判定,是安全 檢測的內容。

在搭建一個合理的安全檢測流程時,不能直接進入對代碼分析的階段,在此之前搞清楚用戶代碼生命周期涉及的各個階段是必要的,只有充分了解被檢測對象和檢測目標的基礎上,才能給出一個合理的流程:

  • 誰寫的代碼:代碼編寫的用戶及用戶形象

  • 代碼寫成什麽樣:代碼語言、結構、內容等內容分析

  • 代碼怎麽用:代碼運行、功能、效果等調用分析

  • 如何檢測:設計合理檢測流程

用戶形象 -“誰寫的代碼”

用戶形象及背景的分析,往往是在純技術實現過程中容易忽略的問題。

在一開始搭建網易貴金屬量化平臺的時候,大部分精力放置在基於用戶代碼本身的安全檢測方案上,忽略了對用戶角度的考慮,導致在沒有對用戶的形象及能力有一個正確評估的情形下構建的安全檢測方案,總出現“出乎意料”、“覆蓋不全”等情況,用戶代碼總超出預想範圍。反思後,我們更換了切入點,先從用戶形象和可能的行為入手,重新構思並設計了檢測流程,使得檢測的覆蓋方面大大增加,實現起來也更為容易。

用戶形象,主要是明確代碼編寫的用戶主體,究竟是以何種狀態和知識水平參與到代碼編寫過程中的,需要明確的用戶形象相關的內容主要可以涵蓋:

  • 用戶編程知識背景:用戶是否具備相關編程語言的編程經驗,以及編程語言的熟悉程度。最不利的假設,如果用戶甚至沒接觸過對應的編程語言,直接在代碼demo上做修改,一定會出現各種各樣的錯誤。

  • 用戶業務知識背景:用戶是否具備代碼涉及業務的知識背景,以及業務知識的掌握程度。特殊場景下,用戶代碼編寫一業務背景相對較明確的代碼時,如果對業務知識背景掌握不是很好,寫出來的代碼可能未必符合合理的邏輯。

  • 用戶操作引導效果:用戶是否會按照平臺給出的引導進行操作。如果用戶對幫助文檔等引導未做到詳細閱讀以及完全理解,用戶代碼在結構和邏輯上可能都會出問題。

  • 用戶意圖:用戶使用平臺的意圖是不明確的,有一定可能會存在惡意用戶以破壞平臺為目的,利用在線編譯器進行破壞。

對上述關註點,不能對用戶的情況抱有幻想,所做的用戶形象假設應考慮最不利的情況。簡而言之:

用戶代碼永遠是不可信的。

案例說明

網易貴金屬量化平臺(上一篇文章中已做介紹),提供Java在線編譯器,用戶代碼內容為量化投資策略邏輯描述,用戶形象中構想的一些不利情況下的假設:

  1. 用戶背景不盡相同,有些用戶之前可能未接觸過Java,不能奢求代碼語法合法性

  2. 用戶技術水平是參差不齊的,代碼寫成什麽樣都有可能,不能奢求代碼規範

  3. 即便是幫助文檔寫的再好,用戶也未必會看的完全,不能奢求代碼操作合理性

  4. 用戶未必會按照你的要求規範,老老實實按照給出的Demo流程書寫,不能奢求代碼符合預定流程

  5. 用戶未必清楚一個量化策略從何開始、到哪結束、需要包含什麽內容,不能奢求代碼邏輯合理性

  6. 可能出現一些惡意用戶,目的就是來搞破壞的,不能奢求代碼安全性

首先把用戶代碼可能觸發安全問題的原因明確後,再根據不同的原因設計對應的解決方案,就會大大提升安全檢測流程的有效性。

代碼內容分析 -“代碼寫成什麽樣”

代碼內容分析,主要是分析代碼涉及的內容、結構等方面的情況,以方便在未獲得用戶代碼之前,對用戶代碼可能涉及和存在的內容作出預判,從而構思應對思路,關註點涵蓋:

  • 代碼結構:以Java為例,可以明確是否以類為主體,是否包含必要的方法,是否有明確的方法結構

  • 業務邏輯:代碼本身是否存在明確的業務邏輯結構

  • 包含引用:代碼是否必須引用其他類,或者類中的某個方法

  • 數據交互:用戶代碼和平臺間存在哪些涉及到數據交互的內容,輸入輸出的內容。

以上的關註點明確後,配合平臺在用戶代碼構建階段對代碼結構的固定,基本可以在用戶代碼獲得前,明確用戶代碼的大致過程。

案例說明

網易貴金屬量化平臺,代碼結構固定,必須實現策略類模板接口(上一篇文章有介紹),業務邏輯上非常明確,策略代碼的行為抽象主要包含:

技術分享圖片

通過對策略代碼的行為抽象,即可明確代碼所包含的所有業務、業務對應類引用以及會產生數據交互的內容。上圖中將行為抽象為樹結構,對行為點以範疇進行區分,並明確標記輸入(I)、輸出(O)等產生數據交互的關註點,以便之後根據此構建代碼檢測流程。

代碼使用分析 -“代碼怎麽用”

用戶代碼最終需要加載至運行環境進行調用的,在平臺調用用戶代碼的過程中,調用細節也會影響到安全檢測的規劃及構思細節。關於使用分析的關註點,可以從以下方面考慮:

  • 編譯過程:編譯過程細節

    • 語言是否需要編譯

    • 編譯是否伴隨診斷,而診斷過程中包含的診斷內容

    • 編譯結果存儲形式(文件、數據庫或內存)

  • 調用過程:平臺調用細節

    • 調用用戶代碼中的何類

    • 調用用戶代碼中的何方法

    • 調用方式(一次性調用、循環觸發、定時觸發、事件觸發等)

    • 調用期間數據交互(內容分析基礎上,分析數據最初來源及最終流向,文件、數據庫、內存等)

  • 運行結束:結束用戶代碼留存情況

    • 調用後是否會被平臺用作其他功用

    • 調用後代碼編譯類是否會被類加載器剔除

    • 調用後用戶代碼是否還會留存在系統內

通過使用分析,可以規劃出在整個生命周期內,用戶代碼跟平臺產生交互的部分,這些不同層次的交互中可能會對系統安全產生威脅,故在安全檢測流程設計時,應結合不同的交互層次給出不同檢測策略。

案例說明

網易貴金屬量化平臺(前文對案例有所介紹),在使用分析的流程上大概可簡述為:

  1. 編譯過程:Java語言,需要進行編譯,編譯過程伴隨診斷,最終編譯結果以類字節碼的形式存在,無需落地

  2. 調用過程:

    1. 用戶代碼為策略類的實現,調用時,該類被加載至服務器的類加載器。

    2. 調用策略類中的init方法以完成該策略類對象初始化、循環調用handle方法以在行情的每一個調度周期內完成相應動作、最終調用onExit方法來完成策略調用過程。

    3. 數據交互上,用戶策略類在使用過程中,會讀取內存中的行情信息,並根據策略邏輯內容產生交易信息(開倉信息、平倉信息)。

  3. 運行結束:用戶代碼在調用後,用戶策略類將不會做其他使用,類加載器中將把該類從加載器中剔除,策略類的實例對象將會被GC回收。

安全檢測流程 -“如何檢測”

上述流程後,用戶代碼的生命周期及使用場景基本描繪完全,可根據具體場景構建安全檢測流程。

布局

不要奢求安全檢測能夠一次性完成,即便是一次性能夠實現安全檢測的目的,代價上也是得不償失的。

安全檢測應分布於代碼生命周期的各個階段。

能夠一次性檢測的方法,理論上一定會部署在整個代碼生命周期內足夠靠後的位置,一旦用戶代碼被檢測出安全問題不在被平臺運行時,之前的所有操作都是白費的;如果采用將安全檢測分布於生命周期的各個階段的方案,不同階段解決不同的安全檢測問題,能夠盡早發現代碼的安全問題,從而盡早打斷,減少不必要的操作以及資源消耗。

檢測內容及目的

簡而言之,檢測的目的就是:保證用戶代碼僅在允許範圍內可用。

以此為根本目的,檢測內容的關註點可包含:

  • 用戶代碼是否使用了規定範圍外的類

  • 用戶代碼是否使用了規定範圍外的方法

  • 用戶代碼是否存在了規定範圍外的行為

  • 用戶代碼是否存在了不易檢測的不良行為(大內存使用、長時間線程占用)

以上內容相對較為抽象,後文將結合具體案例,對使用到的相關技術及具體細節作出介紹。

案例說明

網易貴金屬量化平臺,隨著對Java在線編譯器認知的不斷加深,安全檢測流程也在不斷的健全。

最簡單的版本

用戶可以自主導入JDK相關包,平臺補充API相關包。源代碼生成後,利用JavaComplier中的診斷信息,即能完全判定某策略類是否在當前的項目環境中可運行的。

面臨的問題:

  • 未對import導入的包做限制,用戶可使用JDK所有類

  • 即便對import做限制後,默認導入的java.lang以及當前類所在包是默認導入的,其中的類無法限制導入

導致的問題:

  • 用戶可以使用System等系統相關類,影響系統狀態

  • 用戶可以向外部發送網絡請求

  • 用戶可以自己創建線程

  • ...

更新後的檢測流程

通過對用戶代碼的內容分析、運行分析後,得出這樣一個結論:如果只希望用戶使用平臺提供的服務與行為,就必須限制其余行為,但行為的執行實質就是“方法”,但方法的執行主體是“類及對象”,所以“限制行為的實質就是限制類使用”。

以此認識為核心,並按照將檢測流程分布在代碼生命周期的各個階段的思想,量化平臺規劃出的檢測流程如下:

技術分享圖片

檢測流程被分布在了編譯前、編譯時、編譯後的各個流程內,具體內容包括:

  • 編譯前-編譯預檢:

    • 指定所有編譯後的類所在包路徑

    • 檢測文件是否為空

    • 檢測文件是否單獨進行包導入

    • 檢測文件是否包含策略模板接口實現類

    • 檢測類是否唯一以及是否存在內部類

    • 代碼簡單詞法分析:

    • 代碼存放位置限定:

  • 編譯時-編譯診斷:

    • 獲得類源代碼編譯過程中的診斷信息,判定是否存在診斷

  • 編譯後-結果檢查:

    • 對編譯後的.class字節碼進行解析,找到其中涉及到的所有類,進行安全類列表的白名單檢查

    • 類列表安全檢查:

對於編譯前及編譯時涉及到的檢測內容,在前文《代碼在線編譯器(上)- 編輯及編譯》中均有過詳細的介紹,這裏主要介紹下類列表安全檢查的相關內容。

類列表安全檢查

目的就是,找到用戶策略中所有涉及到的類,與類白名單進行比對,驗證這些類是否均在白名單內,如果出現了名單外的類,認為用戶代碼不安全,可拒絕運行,並給用戶作出反饋。

類白名單

類白名單中的類來自兩部分:

  • 平臺提供的API所涉及的類

  • 必須使用的JDK中的類(java.lang中的類也要單獨指明,java.lang中不是所有類都可用)

  • 三方工具包中的類

獲得涉及類

在獲得用戶代碼所有涉及類的過程中,思路有兩種:

  • 從源代碼進行分析:利用編譯原理中的抽象語法樹(Abstract Syntax Tree)

  • 從字節碼進行分析:利用Java字節碼框架,直接分析字節碼構成。

class類字節碼作為代碼編譯後最終體現,可以反饋實際用到的所有類,實際實現過程中,也是選用此方案進行實施的。

使用的字節碼框架為ASM。

ASM使用

簡介

ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,能夠改變類行為,分析類信息,甚至能夠根據要求生成新類。ASM最常用的使用場景就是靜態AOP的實現(例如CGLIB)。

本文使用ASM,分析類信息,從而提煉出類字節碼中使用涉及到的所有類。

class文件結構

在分析class文件之前,先對class結構作出簡要介紹:

技術分享圖片

文件各部分含義如下:

  • Magic:該項存放了一個 Java 類文件的魔數(magic number)和版本信息。一個 Java 類文件的前 4 個字節被稱為它的魔數。每個正確的 Java 類文件都是以 0xCAFEBABE 開頭的,這樣保證了 Java 虛擬機能很輕松的分辨出 Java 文件和非 Java 文件。

  • Version:該項存放了 Java 類文件的版本信息。

  • Constant Pool:該項存放了類中各種文字字符串、類名、方法名和接口名稱、final變量以及對外部類的引用信息等常量。虛擬機必須為每一個被裝載的類維護一個常量池,常量池中存儲了相應類型所用到的所有類型、字段和方法的符號引用。

  • Access_flag:該項指明了該文件中定義的是類還是接口(一個class文件中只能有一個類或接口),同時還指名了類或接口的訪問標誌,如 public,private, abstract 等信息。

  • This Class:指向表示該類全限定名稱的字符串常量的指針。

  • Super Class:指向表示父類全限定名稱的字符串常量的指針。

  • Interfaces:一個指針數組,存放了該類或父類實現的所有接口名稱的字符串常量的指針。

  • Fields:該項對類或接口中聲明的字段進行了細致的描述,僅列出了本類或接口中的字段,並不包括從超類和父接口繼承而來的字段。

  • Methods:該項對類或接口中聲明的方法進行了細致的描述。例如方法的名稱、參數和返回值類型等。需要註意的是,methods 列表裏僅存放了本類或本接口中的方法,並不包括從超類和父接口繼承而來的方法。

  • Class attributes:該項存放了在該文件中類或接口所定義的屬性的基本信息。

在這些內容中涉及到類名的部分為Fileds和Methods,需使用ASM對這一部分內容進行瀏覽。

class文件內容獲取

以一段簡單的包含Fields和Methods的代碼為例,說明一下類在字節碼中的體現形式。

源碼

public class IniBean {private static Logger logger = LoggerFactory.getLogger("inibean");private static AtomicBoolean initFlag = new AtomicBoolean(true);@PostConstructpublic void init() {
logger.info("[IniBean] init method invoke.");if (initFlag.getAndSet(false)) {
refreshIniInfo();
}
}
...

編譯後字節碼

  public void init();
    Code:       0: getstatic     #2                  // Field logger:Lorg/slf4j/Logger;
       3: ldc           #3                  // String [IniBean] init method invoke.
       5: invokeinterface #4,  2            // InterfaceMethod org/slf4j/Logger.info:(Ljava/lang/String;)V
      10: getstatic     #5                  // Field initFlag:Ljava/util/concurrent/atomic/AtomicBoolean;
      13: iconst_0      14: invokevirtual #6                  // Method java/util/concurrent/atomic/AtomicBoolean.getAndSet:(Z)Z
      17: ifeq          24
      20: aload_0      21: invokevirtual #7                  // Method refreshIniInfo:()V
      24: return

編譯後的字節碼中,類名以文件結構(註意此處不是“.”而是以“/”分割的路徑,通過class.getClassName()獲得的是以“.”分割的類包路徑,比對前需要註意轉換)進行體現,利用ASM瀏覽類文件字節碼即可獲得涉及的類列表。

ASM過程
關鍵類說明

簡要說明幾個用到的關鍵類:

  • org.objectweb.asm.ClassVisitor

    /**
    * A visitor to visit a Java class. The methods of this class must be called in
    * the following order: <tt>visit</tt> [ <tt>visitSource</tt> ] [
    * <tt>visitOuterClass</tt> ] ( <tt>visitAnnotation</tt> |
    * <tt>visitTypeAnnotation</tt> | <tt>visitAttribute</tt> )* (
    * <tt>visitInnerClass</tt> | <tt>visitField</tt> | <tt>visitMethod</tt> )*
    * <tt>visitEnd</tt>.
    *

    類訪問器,用於訪問類節點。

  • org.objectweb.asm.ClassReader

    /**
    * A Java class parser to make a {@link ClassVisitor} visit an existing class.
    * This class parses a byte array conforming to the Java class file format and
    * calls the appropriate visit methods of a given class visitor for each field,
    * method and bytecode instruction encountered.
    * 
    ...

    用於讀入類字節碼相關內容,並提供內部元素訪問方法。

  • org.objectweb.asm.tree.ClassNode

    /**
    * A node that represents a class.
    *

    表示一個類,與class文件結構對應,繼承於org.objectweb.asm.ClassVisitor。

  • org.objectweb.asm.ClassWriter

    /**
    * A {@link ClassVisitor} that generates classes in bytecode form. More
    * precisely this visitor generates a byte array conforming to the Java class
    * file format. It can be used alone, to generate a Java class "from scratch",
    * or with one or more {@link ClassReader ClassReader} and adapter class visitor
    * to generate a modified class from one or more existing Java classes.
    *

    類寫入器,並提供了類逐行掃描的過程。

流程簡述
  1. 讀入類字節碼內容內容

    ClassReader cr = new ClassReader(classByte);
  2. 生成類節點對象ClassNode、類編寫器ClassWriter(用於使用AbstractInsnNode)

    this.classNode = new ClassNode();// 初始化classNode-進行類結構粗掃描classReader.accept(classNode, ClassReader.SKIP_DEBUG);// 初始化ASMClassAdapter-進行方法內逐行掃描ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
    classAdapter = new ASMClassAdapter(cw);
    classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);

    3.遍歷Fields

    private Set<String> getClassInFields() {
    Set<String> result = new HashSet<>();// 0.0 獲得所有屬性List<FieldNode> fieldList = this.classNode.fields;for (FieldNode fieldNode : fieldList) {// 1.0 獲得類名String classNameStr = Type.getType(fieldNode.desc).getClassName();// 2.0 將真實類名填寫到類列表內result.add(ASMConstant.pickClassName(classNameStr));
    }return result;
    }

    4.遍歷所有Methods

    private Set<String> getClassInMethods() {
    Set<String> result = new HashSet<>();// 0.0 獲得所有方法List<MethodNode> methodList = this.classNode.methods;for (MethodNode methodNode : methodList) { // 1.1 如果是構造方法,則捕獲當前類類型
     if(ASMConstant.METHOD_TYPE_INIT.equals(methodNode.name)){
     result.add(ASMConstant.pickClassName(this.classNode.name.replaceAll("\\/", "\\.")));
     } 
    
     // 1.2 提取參數
     for(Type argumentType : Type.getArgumentTypes(methodNode.desc)){
         result.add(ASMConstant.pickClassName(argumentType.getClassName()));
     } // 1.3 提取局部變量
     List<LocalVariableNode> lvNodeList = methodNode.localVariables; for (LocalVariableNode lvn : lvNodeList) {
     result.add(ASMConstant.pickClassName(Type.getType(lvn.desc).getClassName()));
     }
    }return result;
    }

    5.Methods內逐行遍歷(前一步只對Methods聲明作出遍歷)

    public void visitInsn(int opcode) {
    Iterator<AbstractInsnNode> itr = this.instructions.iterator(0);while (itr.hasNext()) {
    AbstractInsnNode insn = itr.next();switch (insn.getType()) {case AbstractInsnNode.FIELD_INSN:// 1.0 方法內局部變量String fieldInsnDesc = ((FieldInsnNode) insn).desc;// 1.1 獲得類名String classNameStr = ASMConstant.pickClassName(Type.getType(fieldInsnDesc).getClassName());// 1.2 將真實類名填寫到類列表內this.classSet.add(classNameStr);break;case AbstractInsnNode.METHOD_INSN:// 2.0 方法內調用的方法String methodInsnOwner = ((MethodInsnNode) insn).owner;// 2.1 名稱轉換    this.classSet.add(ASMConstant.pickClassName(methodInsnOwner.replaceAll("\\/", "\\.")));break; case AbstractInsnNode.TYPE_INSN:// 3.0方法內調用的類型String typeInsnDesc = ((TypeInsnNode) insn).desc;this.classSet.add(ASMConstant.pickClassName(typeInsnDesc.replaceAll("\\/", "\\.")));break;
    ...

    經歷以上過程匯總後,即可獲得某類字節碼中涉及的所有類列表,再與明確構建的白名單列表作出比對,即可驗證類使用範疇的安全性。


心得

工程實踐積累經驗,不但需要在過程中提高熟練度,更需要從實踐中抽象模型、整理思路、總結理論,追求技術中由“技”到“術”獲得的沈澱。


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點擊。


相關文章:
【推薦】 [翻譯]pytest測試框架(一)
【推薦】 Flask寫web時cookie的處理
【推薦】 Wireshark對HTTPS數據的解密

代碼在線編譯器(下)- 用戶代碼安全檢測