Repository: sanic-org/sanic

Issue:
Static routes containing ".." don't work
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Sanic does serve static files if the server file path contains "..", instead it returns file not found errors.

### Code snippet

The following doesn't work:
```
from sanic import Sanic

app = Sanic("Example")
app.static("/", "./static/../static/")
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)
```
Attempt to access any files in the static directory will get the error:
`[2023-03-27 04:05:31 -0700] [60813] [ERROR] File not found: path=static/../static, relative_url=test.txt`
Removing the redundant `../static/` will result in the files being correctly served.

### Expected Behavior

The above example should serve all files in the static directory and not return file not found errors.

### How do you run Sanic?

Sanic CLI

### Operating System

linux

### Sanic Version

22.9.1

### Additional context

This issue appears to have been introduced by #2506 or #2508.
The fix might look something like the following:
```
diff --git a/sanic/mixins/static.py b/sanic/mixins/static.py
index bcffbc8..28effc4 100644
--- a/sanic/mixins/static.py
+++ b/sanic/mixins/static.py
@@ -323,7 +323,7 @@ class StaticHandleMixin(metaclass=SanicMeta):
             # python from herping a derp and treating the uri as an
             # absolute path
             unquoted_file_uri = unquote(__file_uri__).lstrip("/")
-            file_path_raw = Path(file_or_directory, unquoted_file_uri)
+            file_path_raw = Path(root_path, unquoted_file_uri)
             file_path = file_path_raw.resolve()
             if (
                 file_path < root_path and not file_path_raw.is_symlink()
```

It looks like the ".." was originally being done on the URI part only but with the #2506 change this is now done on the combination of the root path *and* relative path. The above change instead uses the resolved form of the root path (which won't contain "..").

However, there are likely other options and having more context about what the checks in this area are for would be crucial.

Discussion:
github-code-scanning[bot]: You have successfully added a new CodeQL configuration `.github/workflows/codeql-analysis.yml:analyze/language:python`. As part of the setup process, we have scanned this repository and found 4 existing alerts. Please check the repository [Security tab](/sanic-org/sanic/security/code-scanning?query=pr%3A2728+tool%3ACodeQL+is%3Aopen) to see all alerts.
codecov[bot]: ## [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Patch coverage: **`100.000`**% and no project coverage change.
> Comparison is base [(`6eaab2a`)](https://codecov.io/gh/sanic-org/sanic/commit/6eaab2a7e5be418385856371fdaebe4701f8c4fc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.938% compared to head [(`dd54f48`)](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.938%.

<details><summary>Additional details and impacted files</summary>


```diff
@@                Coverage Diff                @@
##           current-release     #2728   +/-   ##
=================================================
  Coverage           88.938%   88.938%           
=================================================
  Files                   92        92           
  Lines                 6952      6952           
  Branches              1185      1185           
=================================================
  Hits                  6183      6183           
  Misses                 527       527           
  Partials               242       242           
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/\_\_version\_\_.py](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvX192ZXJzaW9uX18ucHk=) | `100.000% <100.000%> (ø)` | |
| [sanic/mixins/static.py](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvbWl4aW5zL3N0YXRpYy5weQ==) | `89.928% <100.000%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/sanic-org/sanic/pull/2728?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1113fa80..f270d1dd 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -4,10 +4,12 @@ on:
   push:
     branches:
       - main
+      - current-release
       - "*LTS"
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
   schedule:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 13e82615..3cfd8726 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -3,12 +3,14 @@ on:
   push:
     branches:
       - main
+      - current-release
       - "*LTS"
     tags:
       - "!*" # Do not execute on tags
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
 jobs:
   test:
diff --git a/.github/workflows/pr-bandit.yml b/.github/workflows/pr-bandit.yml
index 92dc8991..392ac867 100644
--- a/.github/workflows/pr-bandit.yml
+++ b/.github/workflows/pr-bandit.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-docs.yml b/.github/workflows/pr-docs.yml
index 26af6308..9a326db1 100644
--- a/.github/workflows/pr-docs.yml
+++ b/.github/workflows/pr-docs.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml
index 655beffa..a119a3e8 100644
--- a/.github/workflows/pr-linter.yml
+++ b/.github/workflows/pr-linter.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-python310.yml b/.github/workflows/pr-python310.yml
index 5e66deec..7020fcb3 100644
--- a/.github/workflows/pr-python310.yml
+++ b/.github/workflows/pr-python310.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-python311.yml b/.github/workflows/pr-python311.yml
index f0aa49c9..f89ae731 100644
--- a/.github/workflows/pr-python311.yml
+++ b/.github/workflows/pr-python311.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-python37.yml b/.github/workflows/pr-python37.yml
index c0051d33..a0c5be23 100644
--- a/.github/workflows/pr-python37.yml
+++ b/.github/workflows/pr-python37.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-python38.yml b/.github/workflows/pr-python38.yml
index 09e93f3f..67c75054 100644
--- a/.github/workflows/pr-python38.yml
+++ b/.github/workflows/pr-python38.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-python39.yml b/.github/workflows/pr-python39.yml
index ff479459..5c922276 100644
--- a/.github/workflows/pr-python39.yml
+++ b/.github/workflows/pr-python39.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-type-check.yml b/.github/workflows/pr-type-check.yml
index 27511c74..d9ce1d28 100644
--- a/.github/workflows/pr-type-check.yml
+++ b/.github/workflows/pr-type-check.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/.github/workflows/pr-windows.yml b/.github/workflows/pr-windows.yml
index ca8b5879..4ae0b5be 100644
--- a/.github/workflows/pr-windows.yml
+++ b/.github/workflows/pr-windows.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - main
+      - current-release
       - "*LTS"
     types: [opened, synchronize, reopened, ready_for_review]
 
diff --git a/sanic/__version__.py b/sanic/__version__.py
index fd5f6131..3fc900d3 100644
--- a/sanic/__version__.py
+++ b/sanic/__version__.py
@@ -1,1 +1,1 @@
-__version__ = "23.3.0"
+__version__ = "23.3.1"
diff --git a/sanic/mixins/static.py b/sanic/mixins/static.py
index bcffbc82..90c41bfe 100644
--- a/sanic/mixins/static.py
+++ b/sanic/mixins/static.py
@@ -95,7 +95,7 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta):
             )
 
         try:
-            file_or_directory = Path(file_or_directory)
+            file_or_directory = Path(file_or_directory).resolve()
         except TypeError:
             raise TypeError(
                 "Static file or directory must be a path-like object or string"


Test patch:
diff --git a/tests/test_static.py b/tests/test_static.py
index fe2927bf..925a81ae 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -101,6 +101,31 @@ def test_static_file_pathlib(app, static_file_directory, file_name):
     assert response.body == get_file_content(static_file_directory, file_name)
 
 
+@pytest.mark.parametrize(
+    "file_name",
+    [
+        "test.file",
+        "decode me.txt",
+        "python.png",
+        "symlink",
+        "hard_link",
+    ],
+)
+def test_static_file_pathlib_relative_path_traversal(
+    app, static_file_directory, file_name
+):
+    """Get the current working directory and check if it ends with "sanic" """
+    cwd = Path.cwd()
+    if not str(cwd).endswith("sanic"):
+        pytest.skip("Current working directory does not end with 'sanic'")
+
+    file_path = "./tests/static/../static/"
+    app.static("/", file_path)
+    _, response = app.test_client.get(f"/{file_name}")
+    assert response.status == 200
+    assert response.body == get_file_content(static_file_directory, file_name)
+
+
 @pytest.mark.parametrize(
     "file_name",
     [b"test.file", b"decode me.txt", b"python.png"],