Skip to content

Commit 3643e1e

Browse files
jcrawford13jenkins
authored and
jenkins
committed
util/util-security: Fix Credentials fails to load valid yaml credentials
Problem com.twitter.util.security.Credentials fails to load valid yaml credentials, and it loads invalid yaml credentials. For example, `key:!value` is accepted as a valid Map("key", "!value"), but `{"key": "value"}` is rejected as invalid. Solution Replace the buggy custom parser with a vetted 3rdparty one (snakeyaml) Result Users can load valid yaml files, and invalid yaml files are rejected. JIRA Issues: PSEC-10357 Differential Revision: https://phabricator.twitter.biz/D617641
1 parent bb92fd6 commit 3643e1e

File tree

5 files changed

+49
-45
lines changed

5 files changed

+49
-45
lines changed

Diff for: CHANGELOG.rst

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ Note that ``PHAB_ID=#`` and ``RB_ID=#`` correspond to associated messages in com
77
Unreleased
88
----------
99

10+
Breaking API Changes
11+
~~~~~~~~~~~~~~~~~~~~
12+
* util-security: Use snakeyaml to parse yaml instead of a buggy custom yaml
13+
parser. This means that thrown IOExceptions have been replaced by
14+
YAMLExceptions. Additionally, the parser member has been limited to private visibility. ``PHAB_ID=D617641``
15+
16+
New Features
17+
~~~~~~~~~~~~
18+
19+
* util-security: Any valid yaml / json file with string keys and values can
20+
be loaded with `com.twitter.util.security.Credentials`. ``PHAB_ID=D617641``
21+
1022
Runtime Behavior Changes
1123
~~~~~~~~~~~~~~~~~~~~~~~~
1224

Diff for: build.sbt

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ val caffeineLib = "com.github.ben-manes.caffeine" % "caffeine" % "2.9.2"
2525
val jsr305Lib = "com.google.code.findbugs" % "jsr305" % "2.0.1"
2626
val scalacheckLib = "org.scalacheck" %% "scalacheck" % "1.15.4" % "test"
2727
val slf4jApi = "org.slf4j" % "slf4j-api" % slf4jVersion
28+
val snakeyaml = "org.yaml" % "snakeyaml" % "1.24"
29+
2830

2931
def travisTestJavaOptions: Seq[String] = {
3032
// We have some custom configuration for the Travis environment
@@ -623,7 +625,8 @@ lazy val utilSecurity = Project(
623625
name := "util-security",
624626
libraryDependencies ++= Seq(
625627
scalacheckLib,
626-
"org.scalatestplus" %% "scalacheck-1-14" % "3.1.2.0" % "test"
628+
"org.scalatestplus" %% "scalacheck-1-14" % "3.1.2.0" % "test",
629+
snakeyaml
627630
)
628631
).dependsOn(utilCore, utilLogging, utilSecurityTestCerts % "test")
629632

Diff for: util-security/src/main/scala/com/twitter/util/security/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ scala_library(
88
strict_deps = True,
99
tags = ["bazel-compatible"],
1010
dependencies = [
11+
"3rdparty/jvm/org/yaml:snakeyaml",
1112
"util/util-core:util-core-util",
1213
"util/util-logging/src/main/scala/com/twitter/logging",
1314
],

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

+19-31
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,36 @@
1616

1717
package com.twitter.util.security
1818

19-
import java.io.{File, IOException}
20-
19+
import java.io.File
20+
import org.yaml.snakeyaml.Yaml
21+
import scala.collection.JavaConverters._
2122
import scala.io.Source
22-
import scala.jdk.CollectionConverters._
23-
import scala.util.parsing.combinator._
24-
import scala.util.matching.Regex
2523

2624
/**
2725
* Simple helper to read authentication credentials from a text file.
2826
*
29-
* The file's format is assumed to be trivialized yaml, containing lines of the form ``key: value``.
30-
* Keys can be any word character or '-' and values can be any character except new lines.
27+
* The file's format is assumed to be yaml, containing string keys and values.
3128
*/
3229
object Credentials {
33-
object parser extends RegexParsers {
34-
35-
override val whiteSpace: Regex = "(?:\\s+|#.*\\r?\\n)+".r
36-
37-
private[this] val key = "[\\w-]+".r
38-
private[this] val value = ".+".r
39-
40-
def auth: Parser[Tuple2[String, String]] = key ~ ":" ~ value ^^ {
41-
case k ~ ":" ~ v => (k, v)
42-
case _ => throw new MatchError("Failed case matching on auth parser")
43-
}
44-
def content: Parser[Map[String, String]] = rep(auth) ^^ { auths => Map(auths: _*) }
45-
46-
def apply(in: String): Map[String, String] = {
47-
parseAll(content, in) match {
48-
case Success(result, _) => result
49-
case x: Failure => throw new IOException(x.toString)
50-
case x: Error => throw new IOException(x.toString)
51-
}
52-
}
30+
private[this] val parser: ThreadLocal[Yaml] = new ThreadLocal[Yaml] {
31+
override def initialValue(): Yaml = new Yaml()
5332
}
5433

55-
def apply(file: File): Map[String, String] = parser(Source.fromFile(file).mkString)
34+
def byName(name: String): Map[String, String] = {
35+
apply(new File(sys.env.getOrElse("KEY_FOLDER", "/etc/keys"), name))
36+
}
5637

57-
def apply(data: String): Map[String, String] = parser(data)
38+
def apply(file: File): Map[String, String] = {
39+
val fileSource = Source.fromFile(file)
40+
try apply(fileSource.mkString)
41+
finally fileSource.close()
42+
}
5843

59-
def byName(name: String): Map[String, String] = {
60-
apply(new File(System.getenv().asScala.getOrElse("KEY_FOLDER", "/etc/keys"), name))
44+
def apply(data: String): Map[String, String] = {
45+
val result: java.util.Map[String, Any] = parser.get.load(data)
46+
Option(result)
47+
.map(_.asScala.toMap.mapValues(v => if (v == null) "null" else v.toString)).getOrElse(
48+
Map.empty)
6149
}
6250
}
6351

Diff for: util-security/src/test/scala/com/twitter/util/security/CredentialsTest.scala

+13-13
Original file line numberDiff line numberDiff line change
@@ -21,48 +21,48 @@ import org.scalatest.funsuite.AnyFunSuite
2121

2222
class CredentialsTest extends AnyFunSuite with Checkers {
2323
test("parse a simple auth file") {
24-
val content = "username: root\npassword: hellokitty\n"
24+
val content = "username: root\npassword: not_a_password\n"
2525
val result = Credentials(content)
26-
assert(result == Map("username" -> "root", "password" -> "hellokitty"))
26+
assert(result == Map("username" -> "root", "password" -> "not_a_password"))
2727
}
2828

2929
test("parse a more complex auth file") {
3030
val content = """# random comment
3131
32-
username:root
33-
password : last_0f-the/international:playboys
32+
username: root
33+
password: not_a-real/pr0d:password
3434
# more stuff
35-
moar :ok
35+
moar: ok
3636
3737
"""
3838
assert(
3939
Credentials(content) == Map(
4040
"username" -> "root",
41-
"password" -> "last_0f-the/international:playboys",
41+
"password" -> "not_a-real/pr0d:password",
4242
"moar" -> "ok"
4343
)
4444
)
4545
}
4646

4747
test("work for java peeps too") {
48-
val content = "username: root\npassword: hellokitty\n"
48+
val content = "username: root\npassword: not_a_password\n"
4949
val jmap = new Credentials().read(content)
5050
assert(jmap.size() == 2)
5151
assert(jmap.get("username") == "root")
52-
assert(jmap.get("password") == "hellokitty")
52+
assert(jmap.get("password") == "not_a_password")
5353
}
5454

5555
test("handle \r\n line breaks") {
56-
val content = "username: root\r\npassword: hellokitty\r\n"
57-
assert(Credentials(content) == Map("username" -> "root", "password" -> "hellokitty"))
56+
val content = "username: root\r\npassword: not_a_password\r\n"
57+
assert(Credentials(content) == Map("username" -> "root", "password" -> "not_a_password"))
5858
}
5959

6060
test("handle special chars") {
61-
val pass = (0 to 127)
61+
val pass = (32 to 126)
6262
.map(_.toChar)
63-
.filter(c => c != '\r' && c != '\n')
63+
.filter(c => c != '\r' && c != '\n' && c != '\'')
6464
.mkString
65-
val content = s"username: root\npassword: $pass\n"
65+
val content = s"username: root\npassword: '$pass'\n"
6666
assert(Credentials(content) == Map("username" -> "root", "password" -> pass))
6767
}
6868
}

0 commit comments

Comments
 (0)