Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,20 @@ object SjsonnetMainBase {
case e: Error => Left(Error.formatError(e, maxTrace))
}

private def multiOutputPath(multiPath: String, fileName: String): Either[String, os.FilePath] =
Try {
// Multi output names must stay under the requested output directory.
val normalizedFileName = fileName.dropWhile(_ == '/')
(os.FilePath(multiPath) / os.SubPath(normalizedFileName)).asInstanceOf[os.FilePath]
}.toEither.left.map {
case e: IllegalArgumentException
if Option(e.getMessage).exists(_.contains("ups must be zero")) =>
s"openat $fileName: path escapes from parent"
case e: InvalidPathException => s"openat $fileName: ${e.getMessage}"
case e: IllegalArgumentException => s"openat $fileName: ${e.getMessage}"
case e => e.toString
}

private def writeFile(
config: Config,
f: os.Path,
Expand Down Expand Up @@ -692,7 +706,7 @@ object SjsonnetMainBase {
ujson.transform(v, renderer)
writer.toString
}
relPath = (os.FilePath(multiPath) / os.RelPath(f)).asInstanceOf[os.FilePath]
relPath <- multiOutputPath(multiPath, f)
_ <- writeFile(config, relPath.resolveFrom(wd), rendered, trailingNewline)
} yield relPath
}
Expand Down
53 changes: 53 additions & 0 deletions sjsonnet/test/src-jvm/sjsonnet/MainTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,59 @@ object MainTests extends TestSuite {
assert(os.read(multiDest / "world") == expectedWorld + "\n")
}

test("multiRejectsOutputPathsEscapingParent") {
val root = os.temp.dir()
val multiDest = root / "out"
val (res, out, err) =
runMain("""{"../escape.txt": "owned"}""", "--exec", "--multi", multiDest, "--string")
assert(res == 1)
assert(out.isEmpty)
assert(err.contains("path escapes from parent"))
assert(!err.contains("IllegalArgumentException"))
assert(!os.exists(root / "escape.txt"))
}

test("multiRejectsNestedOutputPathsEscapingParent") {
val root = os.temp.dir()
val multiDest = root / "out"
val (res, out, err) =
runMain("""{"a/../../escape.txt": "owned"}""", "--exec", "--multi", multiDest, "--string")
assert(res == 1)
assert(out.isEmpty)
assert(err.contains("path escapes from parent"))
assert(!err.contains("IllegalArgumentException"))
assert(!os.exists(root / "escape.txt"))
}

test("multiAllowsNormalizedOutputPathsInsideParent") {
val root = os.temp.dir()
val multiDest = root / "out"
os.makeDir.all(multiDest)
val (res, out, err) =
runMain("""{"dir/../ok.txt": "ok"}""", "--exec", "--multi", multiDest, "--string")
assert((res, out, err) == ((0, s"$multiDest/ok.txt\n", "")))
assert(os.read(multiDest / "ok.txt") == "ok\n")
}

test("multiNormalizesAbsoluteOutputPathsInsideParent") {
val root = os.temp.dir()
val multiDest = root / "out"
val externalPath = root / "external" / "abs.txt"
val expectedPath = multiDest / os.SubPath(externalPath.toString.dropWhile(_ == '/'))
val (res, out, err) =
runMain(
"{" + ujson.write(externalPath.toString) + ": \"inside\"}",
"--exec",
"--multi",
multiDest,
"--string",
"--create-output-dirs"
)
assert((res, out, err) == ((0, s"$expectedPath\n", "")))
assert(os.read(expectedPath) == "inside\n")
assert(!os.exists(externalPath))
}

test("missingExtStrFileErrorsAsImport") {
checkCliStderrGolden(
"cli_missing_ext_str_file.jsonnet",
Expand Down
Loading