VYPR
Moderate severityNVD Advisory· Published Aug 30, 2021· Updated Aug 4, 2024

YAML deserialization can run untrusted code

CVE-2021-39132

Description

Rundeck is an open source automation service with a web console, command line tools and a WebAPI. Prior to version 3.3.14 and version 3.4.3, an authorized user can upload a zip-format plugin with a crafted plugin.yaml, or a crafted aclpolicy yaml file, or upload an untrusted project archive with a crafted aclpolicy yaml file, that can cause the server to run untrusted code on Rundeck Community or Enterprise Edition. An authenticated user can make a POST request, that can cause the server to run untrusted code on Rundeck Enterprise Edition. The zip-format plugin issues requires authentication and authorization to these access levels, and affects all Rundeck editions:admin level access to the system resource type. The ACL Policy yaml file upload issues requires authentication and authorization to these access levels, and affects all Rundeck editions: create update or admin level access to a project_acl resource, and/orcreate update or admin level access to the system_acl resource. The unauthorized POST request requires authentication, but no specific authorization, and affects Rundeck Enterprise only. Patches are available in versions 3.4.3, 3.3.14

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.rundeck:rundeck-coreMaven
>= 3.4.0, < 3.4.33.4.3
org.rundeck:rundeck-coreMaven
< 3.3.143.3.14

Affected products

1

Patches

1
850d12e21d22

Merge pull request from GHSA-q4rf-3fhx-88pf

https://github.com/rundeck/rundeckGreg SchuelerAug 13, 2021via ghsa
2 files changed · +191 18
  • core/src/main/java/com/dtolabs/rundeck/core/plugins/ScriptPluginProviderLoader.java+19 4 modified
    @@ -16,10 +16,10 @@
     
     /*
     * ScriptFileProviderLoader.java
    -* 
    +*
     * User: Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a>
     * Created: 4/13/11 10:07 AM
    -* 
    +*
     */
     package com.dtolabs.rundeck.core.plugins;
     
    @@ -34,6 +34,8 @@
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.yaml.snakeyaml.Yaml;
    +import org.yaml.snakeyaml.constructor.Constructor;
    +import org.yaml.snakeyaml.nodes.Tag;
     
     import java.io.File;
     import java.io.FileInputStream;
    @@ -394,12 +396,25 @@ static PluginMeta loadMeta(final File jar, final ZipInputStream zipinput) throws
             return null;
         }
     
    +    /**
    +     * define only constructor for single type
    +     */
    +    static class SingleTypeConstructor extends Constructor{
    +        public SingleTypeConstructor(Class<?> clazz) {
    +            super(clazz);
    +            this.yamlConstructors.put(null, undefinedConstructor);
    +            this.yamlConstructors.put(new Tag(clazz), new SubtypeConstructYamlObject());
    +        }
    +        //required because ConstructYamlObject is protected
    +        class SubtypeConstructYamlObject extends ConstructYamlObject{
    +
    +        }
    +    }
         /**
          * return loaded yaml plugin metadata from the stream
          */
         static PluginMeta loadMetadataYaml(final InputStream stream) {
    -        final Yaml yaml = new Yaml();
    -
    +        final Yaml yaml = new Yaml(new SingleTypeConstructor(PluginMeta.class));
             return yaml.loadAs(stream, PluginMeta.class);
         }
     
    
  • core/src/test/groovy/com/dtolabs/rundeck/core/plugins/ScriptPluginProviderLoaderTest.groovy+172 14 modified
    @@ -16,6 +16,10 @@
     package com.dtolabs.rundeck.core.plugins
     
     import com.dtolabs.rundeck.core.plugins.metadata.PluginMeta
    +import com.dtolabs.rundeck.core.plugins.metadata.ProviderDef
    +import org.yaml.snakeyaml.Yaml
    +import org.yaml.snakeyaml.constructor.SafeConstructor
    +import org.yaml.snakeyaml.error.YAMLException
     import spock.lang.Specification
     import spock.lang.Unroll
     
    @@ -45,18 +49,167 @@ class ScriptPluginProviderLoaderTest extends Specification {
     
         def "LoadMetadataYaml"() {
             setup:
    -        File tmpCacheDir = File.createTempDir()
    +            File tmpCacheDir = File.createTempDir()
     
    -        when:
    -        String pluginName = "Test script plugin"
    -        ScriptPluginProviderLoader loader = new ScriptPluginProviderLoader(File.createTempFile("throwaway","unneeded"),tmpCacheDir)
    -        def pluginYaml = createPluginYaml(pluginName,"1.2")
    +            String pluginName = "Test script plugin"
    +            ScriptPluginProviderLoader loader = new ScriptPluginProviderLoader(File.createTempFile("throwaway","unneeded"),tmpCacheDir)
    +            def pluginYaml = createPluginYaml(pluginName,"1.2",[
    +                author:'author',
    +                date:'adate',
    +                version:'aversion',
    +                url:'aurl',
    +                resourcesDir:'adir',
    +                resourcesList:['res1','res2'],
    +                providers:[
    +                    [
    +                        'name':'aprov',
    +                        'service':'aservice',
    +                        'script-file':'afile',
    +                        'script-args':'args',
    +                        'script-interpreter':'interp',
    +                        'interpreter-args-quoted':true,
    +                        'plugin-type':'type',
    +                        'plugin-meta':[
    +                            'meta1':'metaA',
    +                            'meta2':'metaB'
    +                        ],
    +                    ]
    +                ],
    +            ])
    +        when: "load plugin yaml"
    +            def meta = loader.loadMetadataYaml(pluginYaml)
    +
    +        then: "expected values are defined"
    +            meta.name == pluginName
    +            meta.rundeckPluginVersion == "1.2"
    +            meta.date == 'adate'
    +            meta.version == 'aversion'
    +            meta.url == 'aurl'
    +            meta.resourcesDir == 'adir'
    +            meta.resourcesList == ['res1','res2']
    +            meta.providers.size()==1
    +            meta.pluginDefs.size()==1
    +            meta.pluginDefs[0] instanceof ProviderDef
    +            meta.pluginDefs[0].name=='aprov'
    +            meta.pluginDefs[0].service=='aservice'
    +            meta.pluginDefs[0].scriptFile=='afile'
    +            meta.pluginDefs[0].scriptArgs=='args'
    +            meta.pluginDefs[0].scriptInterpreter=='interp'
    +            meta.pluginDefs[0].interpreterArgsQuoted==true
    +            meta.pluginDefs[0].pluginType=='type'
    +            meta.pluginDefs[0].providerMeta==[
    +                'meta1':'metaA',
    +                'meta2':'metaB'
    +            ]
    +    }
    +    def "LoadMetadataYaml args array"() {
    +        setup:
    +        File tmpCacheDir = File.createTempDir()
    +            String pluginName = "Test script plugin"
    +            ScriptPluginProviderLoader loader = new ScriptPluginProviderLoader(File.createTempFile("throwaway","unneeded"),tmpCacheDir)
    +            def pluginYaml = createPluginYaml(pluginName,"1.2",[
    +                author:'author',
    +                date:'adate',
    +                version:'aversion',
    +                url:'aurl',
    +                resourcesDir:'adir',
    +                resourcesList:['res1','res2'],
    +                providers:[
    +                    [
    +                        'name':'aprov',
    +                        'service':'aservice',
    +                        'script-file':'afile',
    +                        'script-args':[
    +                            'args1',
    +                            'args2'
    +                        ],
    +                        'script-interpreter':'interp',
    +                        'interpreter-args-quoted':true,
    +                        'plugin-type':'type',
    +                        'plugin-meta':[
    +                            'meta1':'metaA',
    +                            'meta2':'metaB'
    +                        ],
    +                    ]
    +                ],
    +            ])
    +        when: "yaml has script-args with a sequence value"
             def meta = loader.loadMetadataYaml(pluginYaml)
     
    -        then:
    +        then: "scriptArgsArray is set in the ProviderDef"
             meta.name == pluginName
             meta.rundeckPluginVersion == "1.2"
    -        meta.date == null
    +        meta.date == 'adate'
    +        meta.version == 'aversion'
    +        meta.url == 'aurl'
    +        meta.resourcesDir == 'adir'
    +        meta.resourcesList == ['res1','res2']
    +        meta.providers.size()==1
    +        meta.pluginDefs.size()==1
    +        meta.pluginDefs[0] instanceof ProviderDef
    +        meta.pluginDefs[0].name=='aprov'
    +        meta.pluginDefs[0].service=='aservice'
    +        meta.pluginDefs[0].scriptFile=='afile'
    +        meta.pluginDefs[0].scriptArgs==null
    +        meta.pluginDefs[0].scriptArgsArray.toList()==['args1','args2']
    +        meta.pluginDefs[0].scriptInterpreter=='interp'
    +        meta.pluginDefs[0].interpreterArgsQuoted==true
    +        meta.pluginDefs[0].pluginType=='type'
    +        meta.pluginDefs[0].providerMeta==[
    +            'meta1':'metaA',
    +            'meta2':'metaB'
    +        ]
    +    }
    +
    +    static final String TEST_YAML1='''name: plugin-name
    +version: 1.0
    +rundeckPluginVersion: 1.2
    +providers:
    +    - name: zingbat
    +      plugin-meta:
    +        test: !!java.lang.Object
    +      '''
    +    static final String TEST_YAML2='''name: plugin-name
    +version: 1.0
    +rundeckPluginVersion: 1.2
    +providers:
    +    - !!java.lang.Object
    +      '''
    +    static final String TEST_YAML3='''name: plugin-name
    +version: 1.0
    +rundeckPluginVersion: 1.2
    +resourcesList:
    +    - !!java.lang.Object
    +providers:
    +    - name: zingbat
    +      '''
    +    static final String TEST_YAML4='''name: plugin-name
    +version: 1.0
    +rundeckPluginVersion: 1.2
    +tags:
    +    - !!java.lang.Object
    +providers:
    +    - name: zingbat
    +      '''
    +
    +    def "LoadMetadataYaml unsafe"() {
    +        setup:
    +            File tmpCacheDir = File.createTempDir()
    +            ScriptPluginProviderLoader loader = new ScriptPluginProviderLoader(File.createTempFile("throwaway","unneeded"),tmpCacheDir)
    +            def pluginYaml =  new ByteArrayInputStream(testYaml.bytes)
    +        when: "load yaml with java class tags"
    +            def meta = loader.loadMetadataYaml(pluginYaml)
    +
    +        then: "should throw exception"
    +            YAMLException e = thrown()
    +            e.message.contains 'could not determine a constructor for the tag tag:yaml.org,2002:java.lang.Object'
    +        where:
    +            testYaml<<[
    +                TEST_YAML1,
    +                TEST_YAML2,
    +                TEST_YAML3,
    +                TEST_YAML4,
    +            ]
         }
     
         @Unroll
    @@ -86,13 +239,18 @@ class ScriptPluginProviderLoaderTest extends Specification {
         }
     
     
    +    /**
    +     * create yaml stream
    +     * @param pluginName `name` value
    +     * @param pluginVersion `rundeckPluginVersion` value
    +     * @param props additional yaml structure
    +     * @return inputstream of yaml string representing the structure
    +     */
         ByteArrayInputStream createPluginYaml(String pluginName, String pluginVersion, Map props = [:]) {
    -        StringBuilder yaml = new StringBuilder()
    -        yaml.append("name: ${pluginName}\n")
    -        yaml.append("rundeckPluginVersion: ${pluginVersion}\n")
    -        props.each { k, v ->
    -            yaml.append("$k : ${v}\n")
    -        }
    -        new ByteArrayInputStream(yaml.toString().bytes)
    +        Yaml yaml = new Yaml()
    +        def map = [name:pluginName,rundeckPluginVersion: pluginVersion] + props
    +        def writer=new StringWriter()
    +        yaml.dump(map,writer)
    +        new ByteArrayInputStream(writer.toString().bytes)
         }
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.