This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/main by this push:
new 1379d7b85 escape html in FileAndResourceDirectives (#1191)
1379d7b85 is described below
commit 1379d7b854fcd6a8733608eab2de91199875a103
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Jul 31 10:03:49 2026 +0100
escape html in FileAndResourceDirectives (#1191)
* escape html in FileAndResourceDirectives
* Update FileAndResourceDirectivesSpec.scala
* Update FileAndResourceDirectives.scala
* Update FileAndResourceDirectives.scala
---
.../directives/FileAndResourceDirectivesSpec.scala | 20 ++++++++++++
.../directives/FileAndResourceDirectives.scala | 36 +++++++++++++++++++---
2 files changed, 51 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 5d7afb139..77a262b03 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
@@ -535,6 +535,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 c425568c8..19a6c9955 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
@@ -400,6 +400,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
@@ -412,15 +429,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]