This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch 1.4.x
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/1.4.x by this push:
new 77532a0e5 escape html in FileAndResourceDirectives (#1191) (#1329)
77532a0e5 is described below
commit 77532a0e5c4597c5b3cf201594b0d7f450c1706b
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Oct 6 19:21:34 2026 +0100
escape html in FileAndResourceDirectives (#1191) (#1329)
* escape html in FileAndResourceDirectives
* Update FileAndResourceDirectivesSpec.scala
* Update FileAndResourceDirectives.scala
* Update FileAndResourceDirectives.scala
(cherry picked from commit 1379d7b854fcd6a8733608eab2de91199875a103)
---
.../directives/FileAndResourceDirectivesSpec.scala | 21 +++++++++++++
.../directives/FileAndResourceDirectives.scala | 36 +++++++++++++++++++---
2 files changed, 52 insertions(+), 5 deletions(-)
diff --git
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala
index 9ba9fee64..de86ceaa5 100644
---
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala
+++
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala
@@ -16,6 +16,7 @@ package directives
import java.io.File
import java.nio.charset.StandardCharsets
+import java.nio.file.Files
import scala.concurrent.duration._
import scala.util.Properties
@@ -546,6 +547,26 @@ class FileAndResourceDirectivesSpec extends RoutingSpec
with Inspectors with Ins
shouldReject("..%c1%9c", warnings = 0)
}
+ "escape HTML special characters in file names to prevent XSS" in {
+ // Only test characters that are valid in filenames on all platforms
(Windows, macOS, Linux).
+ // Windows forbids " < > | : * ? \ / in filenames.
+ val dir = Files.createTempDirectory("pekko-xss-test").toFile
+ try {
+ writeAllText("ampersand", new File(dir, "a&b.txt"))
+ writeAllText("apostrophe", new File(dir, "a'b.txt"))
+ Get() ~>
withSettings(settings)(listDirectoryContents(dir.getAbsolutePath)) ~> check {
+ val body = responseAs[String]
+ body should include("a&b.txt")
+ (body should not).include("a&b.txt")
+ body should include("a'b.txt")
+ (body should not).include("a'b.txt")
+ }
+ } finally {
+ dir.listFiles().foreach(_.delete())
+ dir.delete()
+ }
+ }
+
}
def prep(s: String) = s.stripMarginWithNewline("\n")
diff --git
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectives.scala
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectives.scala
index e8a4120cb..63a036f19 100644
---
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectives.scala
+++
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/FileAndResourceDirectives.scala
@@ -417,6 +417,23 @@ object DirectoryListing {
|</html>
|""".stripMarginWithNewline("\n").split('$')
+ private def escapeHtml(s: String): String = {
+ val sb = new java.lang.StringBuilder(s.length + 16)
+ var i = 0
+ while (i < s.length) {
+ s.charAt(i) match {
+ case '&' => sb.append("&")
+ case '<' => sb.append("<")
+ case '>' => sb.append(">")
+ case '"' => sb.append(""")
+ case '\'' => sb.append("'")
+ case c => sb.append(c)
+ }
+ i += 1
+ }
+ sb.toString
+ }
+
def directoryMarshaller(renderVanityFooter: Boolean):
ToEntityMarshaller[DirectoryListing] =
Marshaller.StringMarshaller.wrap(MediaTypes.`text/html`) { listing =>
val DirectoryListing(path, isRoot, files) = listing
@@ -429,15 +446,24 @@ object DirectoryListing {
def maxNameLength(seq: Seq[(File, String)]) = if (seq.isEmpty) 0 else
seq.map(_._2.length).max
val maxNameLen = math.max(maxNameLength(directoryFilesAndNames) + 1,
maxNameLength(fileFilesAndNames))
val sb = new java.lang.StringBuilder
-
sb.append(html(0)).append(path).append(html(1)).append(path).append(html(2))
+ val escapedPath = escapeHtml(path)
+
sb.append(html(0)).append(escapedPath).append(html(1)).append(escapedPath).append(html(2))
if (!isRoot) {
val secondToLastSlash = path.lastIndexOf('/', path.lastIndexOf('/',
path.length - 1) - 1)
- sb.append("<a href=\"%s/\">../</a>\n".format(path.substring(0,
secondToLastSlash)))
+ sb.append("<a
href=\"%s/\">../</a>\n".format(escapeHtml(path.substring(0,
secondToLastSlash))))
}
def lastModified(file: File) =
DateTime(file.lastModified).toIsoLikeDateTimeString
- def start(name: String) =
- sb.append("<a href=\"").append(path +
name).append("\">").append(name).append("</a>")
- .append(" " * (maxNameLen - name.length))
+ def start(name: String) = {
+ val escapedName = escapeHtml(name)
+ sb.append("<a
href=\"").append(escapeHtml(path)).append(escapedName).append("\">").append(escapedName).append(
+ "</a>")
+ var padding = maxNameLen - name.length
+ while (padding > 0) {
+ sb.append(' ')
+ padding -= 1
+ }
+ sb
+ }
def renderDirectory(file: File, name: String) =
start(name + '/').append("
").append(lastModified(file)).append('\n')
def renderFile(file: File, name: String) = {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]