diff --git a/rules/java/lang/deserialization_of_user_input.yml b/rules/java/lang/deserialization_of_user_input.yml new file mode 100644 index 000000000..0d8b9fbde --- /dev/null +++ b/rules/java/lang/deserialization_of_user_input.yml @@ -0,0 +1,94 @@ +imports: + - java_shared_lang_instance + - java_shared_lang_user_input +patterns: + - pattern: $.readObject(); + filters: + - variable: XML_DECODER_WITH_USER_INPUT + detection: java_lang_deserialization_of_user_input_xml_decoder_with_user_input + - pattern: $.fromXML($); + filters: + - variable: XSTREAM + detection: java_shared_lang_instance + scope: cursor + filters: + - variable: JAVA_SHARED_LANG_INSTANCE_TYPE + regex: \A(com\.thoughtworks\.xstream\.)?(XStream)\z + - variable: INPUT + detection: java_lang_deserialization_of_user_input_source + scope: result + - pattern: $.read(); + filters: + - variable: OBJECT_INPUT_STREAM + detection: java_lang_deserialization_of_user_input_object_input_stream_with_user_input +auxiliary: + - id: java_lang_deserialization_of_user_input_xml_decoder_with_user_input + patterns: + - pattern: new $($); + filters: + - variable: XML_DECODER + regex: \A(javax\.beans\.)?XMLDecoder\z + - variable: INPUT + detection: java_lang_deserialization_of_user_input_source + scope: result + - id: java_lang_deserialization_of_user_input_object_input_stream_with_user_input + patterns: + - pattern: new $($); + filters: + - variable: OBJECT_INPUT_STREAM + regex: \A(java\.io\.)?ObjectInputStream\z + - variable: INPUT + detection: java_lang_deserialization_of_user_input_source + scope: result + - id: java_lang_deserialization_of_user_input_source + patterns: + - pattern: $; + filters: + - variable: USER_INPUT + detection: java_shared_lang_user_input + scope: result + - pattern: $; + filters: + - variable: INPUT_STREAM + detection: java_shared_lang_instance + scope: cursor + filters: + - variable: JAVA_SHARED_LANG_INSTANCE_TYPE + regex: \A(java\.io\.)?(FileInputStream|InputStream)\z + - not: + variable: INPUT_STREAM + detection: java_lang_deserialization_of_user_input_known_resource + - id: java_lang_deserialization_of_user_input_known_resource + patterns: + - pattern: XmlDecodeUtil.class.getResourceAsStream() +languages: + - java +metadata: + description: Unsanitized user input in deserialization method + remediation_message: | + ## Description + + It is bad practice to deserialize untrusted data, such as data that comes + from params or cookies, without sufficient verification. Attackers can + transfer payloads or malicious code via serialized data, and deserializing + such data puts your application at risk. + + ## Remediations + + ❌ Do not deserialize untrusted data + + ❌ Avoid `XMLEncoder` and `XMLDecoder` classes, as these are not recommended + + ✅ Prefer pure (data-only) and language-agnostic (de)serialization formats such as JSON. Avoiding language-specific (de)serialization formats reduces the risk of attackers manipulating the deserialization process for malicious purposes. + + ✅ Prefer (de)serialization methods that allow you to specify the object types that are allowed to be deserialized + + ❌ Never permit the (de)serialization of base object types (like `Object`). + + ## Resources + - [OWASP XEE prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java) + - [OWASP Deserialization cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html) + cwe_id: + - 502 + id: java_lang_deserialization_of_user_input + documentation_url: https://docs.bearer.com/reference/rules/java_lang_deserialization_of_user_input diff --git a/tests/java/lang/deserialization_of_user_input/test.js b/tests/java/lang/deserialization_of_user_input/test.js new file mode 100644 index 000000000..82ff50c38 --- /dev/null +++ b/tests/java/lang/deserialization_of_user_input/test.js @@ -0,0 +1,18 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("deserialization_of_user_input", () => { + const testCase = "main.java" + + const results = invoke(testCase) + + expect(results.Missing).toEqual([]) + expect(results.Extra).toEqual([]) + }) +}) \ No newline at end of file diff --git a/tests/java/lang/deserialization_of_user_input/testdata/main.java b/tests/java/lang/deserialization_of_user_input/testdata/main.java new file mode 100644 index 000000000..6c7a47bff --- /dev/null +++ b/tests/java/lang/deserialization_of_user_input/testdata/main.java @@ -0,0 +1,41 @@ +// Use bearer:expected java_lang_deserialization_of_user_input to flag expected findings + +import java.beans.XMLDecoder; +import java.io.InputStream; +import java.io.ObjectInputStream; +import com.thoughtworks.xstream.XStream; + +public class Foo { + public static void bad(InputStream in) { + XMLDecoder d = new XMLDecoder(in); + // bearer:expected java_lang_deserialization_of_user_input + Object result = d.readObject(); + d.close(); + } + + public static void bad2(HttpServletRequest req, HttpServletResponse res) { + XMLDecoder d = new XMLDecoder(req.getInputStream()); + // bearer:expected java_lang_deserialization_of_user_input + Object result = d.readObject(); + d.close(); + } + + public Object bad3(InputStream in) { + XStream xs = new XStream(); + // bearer:expected java_lang_deserialization_of_user_input + return xs.fromXML(in); + } + + protected void bad4(HttpServletRequest req, HttpServletResponse res) { + ObjectInputStream s = new ObjectInputStream(req.getInputStream()); + // bearer:expected java_lang_deserialization_of_user_input + Object result = s.read(); + } + + public static void ok(String filename) { + InputStream in = XmlDecodeUtil.class.getResourceAsStream(filename); + XMLDecoder d = new XMLDecoder(in); + Object result = d.readObject(); + d.close(); + } +} \ No newline at end of file