|
12 | 12 | * limitations under the License. |
13 | 13 | */ |
14 | 14 | package wvlet.airframe.http |
15 | | -import java.io.File |
| 15 | +import java.io.{File, IOException} |
16 | 16 | import java.net.URL |
17 | | - |
18 | 17 | import wvlet.airframe.control.Control |
19 | 18 | import wvlet.airframe.http.HttpMessage.Response |
20 | 19 | import wvlet.log.LogSupport |
21 | 20 | import wvlet.log.io.{IOUtil, Resource} |
22 | 21 |
|
| 22 | +import java.nio.file.Paths |
23 | 23 | import scala.annotation.tailrec |
| 24 | +import scala.util.Try |
24 | 25 |
|
25 | 26 | /** |
26 | 27 | * Helper for returning static contents |
27 | 28 | */ |
28 | 29 | object StaticContent extends LogSupport { |
29 | 30 |
|
30 | 31 | trait ResourceType { |
| 32 | + def basePath: String |
31 | 33 | def find(relativePath: String): Option[URL] |
| 34 | + |
| 35 | + private val canonicalBasePath: Try[String] = Try(new File(basePath).getCanonicalPath) |
| 36 | + |
| 37 | + // Helper to check if a potential resource path is truly within the base path |
| 38 | + protected def isPathInsideBase(resourceFile: File): Boolean = { |
| 39 | + canonicalBasePath |
| 40 | + .flatMap { cbPath => |
| 41 | + Try(resourceFile.getCanonicalPath).map { rcPath => |
| 42 | + // Check if the resource's canonical path starts with the base's canonical path, |
| 43 | + // ensuring it's truly contained within. Handle the separator correctly. |
| 44 | + rcPath.startsWith(cbPath) && |
| 45 | + (rcPath.length == cbPath.length || // Exactly the base path itself |
| 46 | + rcPath.charAt(cbPath.length) == File.separatorChar || // Starts with base path + separator |
| 47 | + cbPath == "/") // Special case for root base path |
| 48 | + } |
| 49 | + }.recover { case e: IOException => |
| 50 | + // Error getting canonical paths likely indicates issues (permissions, non-existent?) |
| 51 | + logger.warn(s"Failed to get canonical path for comparison: ${e.getMessage}", e) |
| 52 | + false |
| 53 | + }.getOrElse(false) // Default to false if any Try failed |
| 54 | + } |
32 | 55 | } |
33 | 56 |
|
34 | 57 | case class FileResource(basePath: String) extends ResourceType { |
35 | 58 | override def find(relativePath: String): Option[URL] = { |
36 | 59 | val f = new File(s"${basePath}/${relativePath}") |
37 | | - if (f.exists()) { |
| 60 | + if (f.exists() && f.isFile() && isPathInsideBase(f)) { |
38 | 61 | Some(f.toURI.toURL) |
39 | 62 | } else { |
40 | 63 | None |
@@ -63,7 +86,16 @@ object StaticContent extends LogSupport { |
63 | 86 | } |
64 | 87 | } |
65 | 88 |
|
66 | | - loop(0, path.split("/").toList) |
| 89 | + // Check for null characters |
| 90 | + if (path.contains("\u0000")) { |
| 91 | + false |
| 92 | + } else if (path.startsWith("/") || path.isEmpty || path.contains("//")) { |
| 93 | + false |
| 94 | + } else if (Try(Paths.get(path)).isFailure) { |
| 95 | + false |
| 96 | + } else { |
| 97 | + loop(0, path.split("/").toList.filter(_.nonEmpty)) |
| 98 | + } |
67 | 99 | } |
68 | 100 |
|
69 | 101 | private def findContentType(filePath: String): String = { |
@@ -116,7 +148,7 @@ object StaticContent extends LogSupport { |
116 | 148 |
|
117 | 149 | import wvlet.airframe.http.StaticContent.* |
118 | 150 |
|
119 | | -case class StaticContent(resourcePaths: List[StaticContent.ResourceType] = List.empty) { |
| 151 | +case class StaticContent(resourcePaths: List[StaticContent.ResourceType] = List.empty) extends LogSupport { |
120 | 152 | def fromDirectory(basePath: String): StaticContent = { |
121 | 153 | this.copy(resourcePaths = FileResource(basePath) :: resourcePaths) |
122 | 154 | } |
|
0 commit comments