java.lang.security.jackson-unsafe-deserialization.jackson-unsafe-deserialization

profile photo of semgrepsemgrep
Author
unknown
Download Count*

When using Jackson to marshall/unmarshall JSON to Java objects, enabling default typing is dangerous and can lead to RCE. If an attacker can control $JSON it might be possible to provide a malicious JSON which can be used to exploit unsecure deserialization. In order to prevent this issue, avoid to enable default typing (globally or by using "Per-class" annotations) and avoid using Object and other dangerous types for member variable declaration which creating classes for Jackson based deserialization.

Run Locally

Run in CI

Defintion

rules:
  - id: jackson-unsafe-deserialization
    patterns:
      - pattern-either:
          - patterns:
              - pattern-inside: |
                  ObjectMapper $OM = new ObjectMapper(...);
                  ...
              - pattern-inside: |
                  $OM.enableDefaultTyping();
                  ...
              - pattern: $OM.readValue($JSON, ...);
          - patterns:
              - pattern-inside: |
                  class $CLASS {
                    ...
                    @JsonTypeInfo(use = Id.CLASS,...)
                    $TYPE $VAR;
                    ...
                  }
              - metavariable-regex:
                  metavariable: $TYPE
                  regex: (Object|Serializable|Comparable)
              - pattern: $OM.readValue($JSON, $CLASS.class);
          - patterns:
              - pattern-inside: |
                  class $CLASS {
                    ...
                    ObjectMapper $OM;
                    ...
                    $INITMETHODTYPE $INITMETHOD(...) {
                      ...
                      $OM = new ObjectMapper();
                      ...
                      $OM.enableDefaultTyping();
                      ...
                    }
                    ...
                  }
              - pattern-inside: |
                  $METHODTYPE $METHOD(...) {
                    ...  
                  }
              - pattern: $OM.readValue($JSON, ...);
    message: When using Jackson to marshall/unmarshall JSON to Java objects,
      enabling default typing is dangerous and can lead to RCE. If an attacker
      can control `$JSON` it might be possible to provide a malicious JSON which
      can be used to exploit unsecure deserialization. In order to prevent this
      issue, avoid to enable default typing (globally or by using "Per-class"
      annotations) and avoid using `Object` and other dangerous types for member
      variable declaration which creating classes for Jackson based
      deserialization.
    languages:
      - java
    severity: WARNING
    metadata:
      category: security
      subcategory:
        - audit
      cwe:
        - "CWE-502: Deserialization of Untrusted Data"
      confidence: MEDIUM
      likelihood: LOW
      impact: HIGH
      owasp:
        - A8:2017 Insecure Deserialization
        - A8:2021 Software and Data Integrity Failures
      references:
        - https://swapneildash.medium.com/understanding-insecure-implementation-of-jackson-deserialization-7b3d409d2038
        - https://cowtowncoder.medium.com/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062
        - https://adamcaudill.com/2017/10/04/exploiting-jackson-rce-cve-2017-7525/
      technology:
        - jackson
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - "Insecure Deserialization "

Examples

jackson-unsafe-deserialization.java

private class Car {
    private Fake variable;

    @JsonTypeInfo(use = Id.CLASS)
    private Object color;
    private String type;

    public Car() {
    }

    public Car(Object color, String type) {
        this.color = color;
        this.type = type;
    }

    public String getColor() {
        return (String) this.color;
    }

    public void setColor(Object color) {
        this.color = color;
    }

    public String getType() {
        return this.type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();

        try {
            // ruleid: jackson-unsafe-deserialization
            Car car = objectMapper.readValue(Paths.get("target/payload.json").toFile(), Car.class);
            System.out.println((car.getColor()));
        } catch (Exception e) {
            System.out.println("Exception raised:" + e.getMessage());
        }

    }

    public static void anotherMain(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        // Disable default typing globally
        // objectMapper.enableDefaultTyping();

        try {
            // ruleid: jackson-unsafe-deserialization
            Car car = objectMapper.readValue(Paths.get("target/payload.json").toFile(), Car.class);
            System.out.println((car.getColor()));
        } catch (Exception e) {
            System.out.println("Exception raised:" + e.getMessage());
        }

    }

    public static void anotherMain2(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
        ObjectMapper objectMapper = new ObjectMapper();

        try {
            // ok: jackson-unsafe-deserialization
            Car car = objectMapper.readValue(Paths.get("target/payload.json").toFile(), Another.class);
            System.out.println((car.getColor()));
        } catch (Exception e) {
            System.out.println("Exception raised:" + e.getMessage());
        }

    }
}

// Additional class to test rule when ObjectMapper is created in a different
// method
@RestController
public class MyController {
    private Test variable;
    private ObjectMapper objectMapper;
    private Test2 variable2;

    @PostConstruct
    public void initialize() {
        this.variable = 123;
        objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        this.variable2 = 456;
    }

    @RequestMapping(path = "/", method = RequestMethod.GET)
    public void redirectToUserInfo(HttpServletResponse response) throws IOException {
        response.sendRedirect("/somewhere");
    }

    @RequestMapping(path = "/vulnerable", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public GenericUser vulnerable(@CookieValue(name = "token", required = false) String token)
            throws JsonParseException, JsonMappingException, IOException {
        byte[] decoded = Base64.getDecoder().decode(token);
        String decodedString = new String(decoded);
        // ruleid: jackson-unsafe-deserialization
        Car obj = objectMapper.readValue(
                decodedString,
                Car.class);
        return obj;
    }
}