forked from Ikalus1988/MisakaNet
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_slugify_path_traversal.py
More file actions
150 lines (122 loc) · 5.82 KB
/
Copy pathtest_slugify_path_traversal.py
File metadata and controls
150 lines (122 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""
Additional regression tests for _slugify() covering path traversal,
null bytes, and Windows reserved characters (the security hardening
gap found after the initial 5-test suite merged in May 2026).
These tests target the real-world attack/edge case surface that
the original tests didn't cover. They were added in response to
EvoMap bounty #cmptjhjjg4ood7i2bkhkov (re-posted #95) which
required: "strip ../, ..\\, null bytes, reserved Windows chars".
The current slugify() implementation (as of commit
8c5c92a1) handles all of these correctly via its
`re.sub(r"[^a-z0-9\u4e00-\u9fff]+", "-", slug)` regex, but
the test coverage was incomplete. This file closes that gap.
"""
import unittest
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
from new_lesson import _slugify
class TestSlugifyPathTraversal(unittest.TestCase):
"""
Path traversal characters must be neutralized in the output slug.
No generated filename should ever contain a `..` segment or
backslashes (which on Windows can be interpreted as directory
separators).
"""
def test_unix_path_traversal_neutralized(self):
# `../../etc/passwd` should never survive verbatim
result = _slugify("../../etc/passwd")
self.assertNotIn("..", result, f"`..` found in slug: {result!r}")
self.assertNotIn("/", result, f"`/` found in slug: {result!r}")
def test_windows_path_traversal_neutralized(self):
result = _slugify("..\\..\\windows\\system32")
self.assertNotIn("..", result)
self.assertNotIn("\\", result, f"backslash survived: {result!r}")
def test_absolute_unix_path_neutralized(self):
result = _slugify("/etc/passwd")
self.assertNotIn("/", result)
self.assertNotIn("..", result)
def test_absolute_windows_drive_neutralized(self):
result = _slugify("C:\\Windows\\System32")
self.assertNotIn("\\", result)
self.assertNotIn(":", result)
self.assertNotIn("..", result)
def test_trailing_dot_neutralized(self):
# Windows silently strips trailing dots, which can cause
# collisions like `foo.` becoming `foo`. Slugify must
# remove the trailing dot so the actual filename is `foo.md`.
result = _slugify("name.")
self.assertFalse(result.endswith("."),
f"trailing dot survived: {result!r}")
def test_trailing_space_neutralized(self):
# Windows silently strips trailing spaces, again causing
# collisions. Slugify must remove trailing spaces.
result = _slugify("name ")
self.assertFalse(result.endswith(" "),
f"trailing space survived: {result!r}")
class TestSlugifyNullBytes(unittest.TestCase):
"""
Null bytes (\x00) and other control characters can truncate
filenames on POSIX (`foo\x00.md` writes only `foo`) or cause
silent failures on Windows. They must never appear in a slug.
"""
def test_null_byte_stripped(self):
result = _slugify("file\x00name.md")
self.assertNotIn("\x00", result)
# The null byte should be replaced by a hyphen, then collapsed
self.assertNotIn(".", result, f"period survived: {result!r}")
def test_multiple_null_bytes_stripped(self):
result = _slugify("test\x00\x00")
self.assertNotIn("\x00", result)
def test_control_characters_stripped(self):
# Newline, tab, carriage return — none should appear in slug
for ch in ("\n", "\t", "\r"):
result = _slugify(f"foo{ch}bar")
self.assertNotIn(ch, result, f"{ch!r} survived: {result!r}")
class TestSlugifyWindowsReserved(unittest.TestCase):
"""
The full set of Windows reserved names plus a few extra
edge cases. (Original tests covered CON/PRN/NUL/COM1 — these
are the gaps: AUX, LPT1-LPT9, mixed case, and reserved names
with extensions.)
"""
def test_all_reserved_names_prefixed(self):
for name in ["CON", "PRN", "AUX", "NUL",
"COM1", "COM5", "COM9",
"LPT1", "LPT5", "LPT9"]:
with self.subTest(name=name):
result = _slugify(name)
self.assertNotEqual(result, name.lower())
self.assertTrue(result.startswith("safe-"),
f"{name!r} -> {result!r}")
def test_lowercase_reserved_names_prefixed(self):
for name in ["con", "prn", "aux", "nul", "com1", "lpt1"]:
result = _slugify(name)
self.assertTrue(result.startswith("safe-"),
f"{name!r} -> {result!r}")
def test_reserved_with_extension_prefixed(self):
# `CON.txt` should still be flagged because the *stem* is
# reserved. (If the current implementation only checks the
# full slug without the .md, it might miss this.)
result = _slugify("con.md")
# `con.md` becomes `con-md` which is NOT reserved (con is
# only reserved when there's NO extension), so this is OK
# to NOT prefix — verify the regex correctly handles it.
self.assertNotIn(".", result, f"period survived: {result!r}")
class TestSlugifyUnicodeRobustness(unittest.TestCase):
"""
Unicode edge cases beyond the basic Chinese-character handling.
"""
def test_zero_width_space_stripped(self):
# Zero-width space (U+200B) is invisible in editors but
# can cause silent filename differences.
result = _slugify("file\u200bname")
self.assertNotIn("\u200b", result)
def test_combining_characters_decomposed(self):
# `é` (U+00E9) should decompose to `e` (U+0065) under NFKD
result = _slugify("café")
self.assertEqual(result, "cafe",
f"NFKD failed: {result!r}")
if __name__ == "__main__":
unittest.main(verbosity=2)