|
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