Skip to main content

SnakeYaml 2.0: Solving the unsafe deserialization vulnerability

Écrit par:
blog-feature-supply-chain-sbom

21 juin 2023

0 minutes de lecture

In the December of last year, we reported CVE-2022-1471 to you. This unsafe deserialization problem could easily lead to arbitrary code execution under the right circumstances. 

In the deep-dive blog post “Unsafe deserialization vulnerability in SnakeYaml (CVE-2022-1471)”, I explained the problems in this library and how it could be executed. The gist of the problem was that by default SnakeYaml parsed the incoming yaml to the generic object type. This creates an opportunity to deserialize other classes that are available on the class path. Regardless of the ClassCastException that is thrown, if the object has already been loaded, the damage is done.

The impact heavily depends on how you use the library. For example, many developers only use yaml to provide configuration to their apps. This vulnerability is only exploitable if you accept yaml from unknown sources — like end users. Nevertheless, we simply can't predict how people will use a library like this, and the default setting should be secure.

Upgrading SnakeYaml with Snyk Open Source

Let’s use Snyk Open Source to find a replacement for the old SnakeYaml library. If I locally run snyk test with the Snyk CLI, I see that there is a replacement available.

blog-snakeyaml-ace-warning

The web interface also tells me that a SnykYaml 2.0 version is available to solve the problem. The only issue is that Spring Boot 3 still ships the 1.x version, so I have to replace the version manually in my Maven or Gradle manifest file.

blog-snakeyaml-snyk-vuln-437

Be aware that manually changing the library to a new major version might break things. Don’t blindly make these changes, and be aware of the potential impact this has on the inner workings of your application. 

Mitigating deserialization with SnakeYaml 2.0

SnakeYaml 2.0 was released in early 2023 to mitigate the default behavior that can lead to possible arbitrary code execution. In this version, the constructor that every new yaml() uses now extends SafeConstructor. As a result, we can only parse a limited set of types. SnakeYaml’s SafeConstrutor can construct standard Java classes like primitives and basic classes like string and map.

Specific types cannot be parsed anymore by default. So, my yaml file below will provide an exception:

!!Gadget ["env"]
Exception in thread "main" Global tag is not allowed: tag:yaml.org,2002:Gadget
 in 'reader', line 1, column 1:
    !!Gadget ["env"]
    ^

Now, the default implementation is no longer vulnerable. However, this is a breaking change, so your initial code likely won’t work anymore.

How to fix my Yaml parsing logic when using SnakeYaml 2.x

First and foremost, we have to make sure we don't use SnakeYaml 1.x anymore. Even the latest Spring Boot version 3.1 does not currently ship with SnakeYaml 2.x. This means we have to upgrade it ourselves in our manifest file. For Maven implementations, we can use the <dependencyManagement> part of the pom file among other things. In Gradle, it is also possible to update transitive dependencies using dependency constraints.

Note that SnakeYaml 2.x breaks API compared to the earlier versions. This means we have to rewrite our yaml parsing implementation to the new safe defaults to make it work again.

Let's consider a very simple domain with two classes: 

  • Person

  • Comment

Person.java:

1public class Person {
2    private String name;
3    private int age;
4
5    private Comment comment;
6
7    public Person() {
8    }
9
10    public Person(String name, int age, Comment class2) {
11        this.name = name;
12        this.age = age;
13        this.comment = class2;
14    }
15    //getters and setters
16}

Comment.java:

1public class Comment {
2
3    private String text;
4    private String dateTime;
5
6    public Comment() {}
7
8    public Comment(String text) {
9        this.text = text;
10        this.dateTime = LocalDateTime.now().toString();
11    }
12    //getters and setters
13}

If you want to create a yaml file from an entity like the above, you likely wrote something similar to the following when using SnakeYaml 1.x:

1var john = new Person("John", 31, new Comment("This is a comment"));
2dumpYaml(john, "file.yaml");
3
4public static void dumpYaml(Person pojo, String filename) throws IOException {
5    Yaml yaml = new Yaml();
6
7    try (FileWriter writer = new FileWriter(filename)) {
8        yaml.dump(pojo, writer);
9    }
10}

This results in the following yaml file:

1!!mypackage.Person
2age: 31
3comment: {dateTime: '2023-06-15T16:53:23.175989', text: This is a comment}
4name: John

Using the SnakeYaml 2.x, the !!mypackage.Person is not excepted anymore. Now we can get rid of the object reference when parsing the object to a yaml file. However, there’s still a problem with yaml files that were exported before this migration.

Luckily, when can still solve this problem! When parsing to a specific object, you can set the constructor the parser needs to use. In addition, we can add a specific TagInspector to the LoaderOptions that allows our package tag. This allows you to only permit yaml files that fit your object and are backward compatible with the yaml created previously with the 1.x versions.

1public static Person parseYaml(String filename) throws IOException {
2        var loaderoptions = new LoaderOptions();
3        TagInspector taginspector = 
4                tag -> tag.getClassName().equals(Person.class.getName());
5        loaderoptions.setTagInspector(taginspector);
6
7        Yaml yaml = new Yaml(new Constructor(Person.class, loaderoptions));
8
9        try (InputStream in = new FileInputStream(filename)) {
10            // Parse the YAML file into a mypackage.MyYamlClass object
11            Person obj = yaml.load(in);
12            return obj;
13        }
14    }

Additionally, it would be better to remove the reference or the tag to the actual object from your yaml file altogether. This was already possible in earlier versions of SnakeYaml — by adding a representer to your yaml object that maps the tag of the top-level object to map. Below, you see an example of this that’s compatible with SnakeYaml 2.x:

1    public static void dumpYaml(Person pojo, String filename) throws IOException {
2        Representer customRepresenter = new Representer(new DumperOptions());
3        customRepresenter.addClassTag(Person.class, Tag.MAP);
4
5        Yaml yaml = new Yaml(new Constructor(Person.class, new LoaderOptions()), 
6                customRepresenter);
7
8        try (FileWriter writer = new FileWriter(filename)) {
9            yaml.dump(pojo, writer);
10        }
11    }

The yaml file will not start with !!mypackage.Person (or similar) anymore. If all your yaml files are clean, you can remove the TagInspector form from your parser.

Stay up to date with Snyk

Staying up to date with all the versions of your library is critical to open source security. Using the SnakeYaml 1.x version can lead to unnecessary security issues if you directly or indirectly accept yaml files from outside sources.

Snyk Open Source can help you find and fix these issues or point you to an alternative version if necessary — like the following example, where we show that updating to at least version 2.0 of SnakeYaml will remove the Arbitrary Code Execution vulnerability.

blog-snakeyaml-snyk-vuln-651

Also, if you connect your git repository to Snyk, we can provide pull requests to keep your dependencies up to date, even if there’s no critical security issue. An ounce of prevention is best, and staying up to date on your libraries helps you minimize the risk of vulnerabilities and reduce the extra work that comes with updating because of a security issue.

blog-snakeyaml-pr-upgrade