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&amp;b.txt")
+          (body should not).include("a&b.txt")
+          body should include("a&#39;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("&amp;")
+        case '<'  => sb.append("&lt;")
+        case '>'  => sb.append("&gt;")
+        case '"'  => sb.append("&quot;")
+        case '\'' => sb.append("&#39;")
+        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]

Reply via email to