Skip to content

Commit 6aee07c

Browse files
committed
fix: reject unpaired JSON surrogates
1 parent dd4f1c3 commit 6aee07c

13 files changed

Lines changed: 88 additions & 10 deletions

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,20 +1395,28 @@ class Evaluator(
13951395
if (result.isInfinite) Error.fail("Overflow", pos)
13961396
Val.cachedNum(pos, result)
13971397
case _: Val.Str =>
1398-
Val.Str.concat(pos, Val.Str(pos, RenderUtils.renderDouble(ld)), r.asInstanceOf[Val.Str])
1398+
Val.Str.concat(
1399+
pos,
1400+
Val.Str(pos, RenderUtils.renderDouble(ld)),
1401+
r.asInstanceOf[Val.Str]
1402+
)
13991403
case _ => failBinOp(l, e.op, r, pos)
14001404
}
14011405
case ls: Val.Str =>
14021406
r match {
1403-
case rs: Val.Str => Val.Str.concat(pos, ls, rs)
1407+
case rs: Val.Str => Val.Str.concat(pos, ls, rs)
14041408
case Val.Num(_, rd) =>
14051409
Val.Str.concat(pos, ls, Val.Str(pos, RenderUtils.renderDouble(rd)))
14061410
case _ => Val.Str.concat(pos, ls, Val.Str(pos, Materializer.stringify(r)))
14071411
}
14081412
case _ =>
14091413
r match {
14101414
case _: Val.Str =>
1411-
Val.Str.concat(pos, Val.Str(pos, Materializer.stringify(l)), r.asInstanceOf[Val.Str])
1415+
Val.Str.concat(
1416+
pos,
1417+
Val.Str(pos, Materializer.stringify(l)),
1418+
r.asInstanceOf[Val.Str]
1419+
)
14121420
case _ =>
14131421
(l, r) match {
14141422
case (lo: Val.Obj, ro: Val.Obj) => ro.addSuper(pos, lo)

sjsonnet/src/sjsonnet/Importer.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,9 @@ object CachedResolver {
377377
new JsonImportVisitor(fileScope, internedStrings, settings)
378378
Some((ujson.ByteArrayParser.transform(content.readRawBytes(), visitor), fileScope))
379379
} catch {
380-
case _: ujson.ParsingFailedException | _: DuplicateJsonKey | _: InvalidJsonNumber |
381-
_: JsonParseDepthExceeded | _: NumberFormatException =>
380+
case _: ujson.ParsingFailedException | _: DuplicateJsonKey |
381+
_: ValVisitor.InvalidUnicodeString | _: InvalidJsonNumber | _: JsonParseDepthExceeded |
382+
_: NumberFormatException =>
382383
None
383384
}
384385
}
@@ -420,7 +421,11 @@ object CachedResolver {
420421
private var key: String = _
421422
def subVisitor: upickle.core.Visitor[?, ?] = self
422423
def visitKey(index: Int): upickle.core.StringVisitor.type = upickle.core.StringVisitor
423-
def visitKeyValue(s: Any): Unit = key = intern(s.toString)
424+
def visitKeyValue(s: Any): Unit = {
425+
val str = s.toString
426+
ValVisitor.rejectUnpairedSurrogates(str)
427+
key = intern(str)
428+
}
424429
def visitValue(v: Val, index: Int): Unit = {
425430
if (!seen.add(key)) throw new DuplicateJsonKey
426431
keys += key
@@ -474,6 +479,7 @@ object CachedResolver {
474479
case str: String => str
475480
case _ => s.toString
476481
}
482+
ValVisitor.rejectUnpairedSurrogates(str)
477483
val unique = intern(str)
478484
Val.Str(pos(index), unique)
479485
}

sjsonnet/src/sjsonnet/ValVisitor.scala

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,31 @@ import upickle.core.{ArrVisitor, ObjVisitor, Visitor}
77

88
import scala.collection.mutable
99

10+
object ValVisitor {
11+
final class InvalidUnicodeString extends RuntimeException(null, null, false, false)
12+
13+
def rejectUnpairedSurrogates(s: CharSequence): Unit = {
14+
var i = 0
15+
val length = s.length
16+
while (i < length) {
17+
val c = s.charAt(i)
18+
if (Character.isHighSurrogate(c)) {
19+
if (i + 1 >= length || !Character.isLowSurrogate(s.charAt(i + 1))) {
20+
throw new InvalidUnicodeString
21+
}
22+
i += 2
23+
} else if (Character.isLowSurrogate(c)) {
24+
throw new InvalidUnicodeString
25+
} else {
26+
i += 1
27+
}
28+
}
29+
}
30+
}
31+
1032
/** Parse JSON directly into a literal `Val` */
11-
class ValVisitor(pos: Position) extends JsVisitor[Val, Val] { self =>
33+
class ValVisitor(pos: Position, rejectUnpairedSurrogates: Boolean = false)
34+
extends JsVisitor[Val, Val] { self =>
1235

1336
override def visitJsonableObject(length: Int, index: Int): ObjVisitor[Val, Val] =
1437
visitObject(length, index)
@@ -27,7 +50,10 @@ class ValVisitor(pos: Position) extends JsVisitor[Val, Val] { self =>
2750
var key: String = _
2851
def subVisitor: Visitor[?, ?] = self
2952
def visitKey(index: Int): upickle.core.StringVisitor.type = upickle.core.StringVisitor
30-
def visitKeyValue(s: Any): Unit = key = s.toString
53+
def visitKeyValue(s: Any): Unit = {
54+
key = s.toString
55+
if (rejectUnpairedSurrogates) ValVisitor.rejectUnpairedSurrogates(key)
56+
}
3157
def visitValue(v: Val, index: Int): Unit = {
3258
cache.put(key, v)
3359
allKeys.put(key, false)
@@ -52,5 +78,8 @@ class ValVisitor(pos: Position) extends JsVisitor[Val, Val] { self =>
5278
}
5379
)
5480

55-
def visitString(s: CharSequence, index: Int): Val = Val.Str(pos, s.toString)
81+
def visitString(s: CharSequence, index: Int): Val = {
82+
if (rejectUnpairedSurrogates) ValVisitor.rejectUnpairedSurrogates(s)
83+
Val.Str(pos, s.toString)
84+
}
5685
}

sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,13 @@ object ManifestModule extends AbstractFunctionModule {
126126
private object ParseJson extends Val.Builtin1("parseJson", "str") {
127127
def evalRhs(str: Eval, ev: EvalScope, pos: Position): Val = {
128128
try {
129-
ujson.StringParser.transform(str.value.asString, new ValVisitor(pos))
129+
ujson.StringParser.transform(
130+
str.value.asString,
131+
new ValVisitor(pos, rejectUnpairedSurrogates = true)
132+
)
130133
} catch {
134+
case _: ValVisitor.InvalidUnicodeString =>
135+
throw Error.fail("Invalid JSON: unpaired surrogate in string", pos)(ev)
131136
case e: ujson.ParseException =>
132137
throw Error.fail("Invalid JSON: " + e.getMessage, pos)(ev)
133138
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseJson('"\\uD800"')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sjsonnet.Error: [std.parseJson] Invalid JSON: unpaired surrogate in string
2+
at [<root>].(error.parsejson_lone_high_surrogate.jsonnet:1:14)
3+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseJson('"\\uDC00"')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sjsonnet.Error: [std.parseJson] Invalid JSON: unpaired surrogate in string
2+
at [<root>].(error.parsejson_lone_low_surrogate.jsonnet:1:14)
3+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseJson('{"\\uD800": 1}')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sjsonnet.Error: [std.parseJson] Invalid JSON: unpaired surrogate in string
2+
at [<root>].(error.parsejson_lone_surrogate_key.jsonnet:1:14)
3+

0 commit comments

Comments
 (0)