-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_security.py
More file actions
307 lines (250 loc) · 11.2 KB
/
test_security.py
File metadata and controls
307 lines (250 loc) · 11.2 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
"""Tests for security utilities."""
import stat
import tempfile
from pathlib import Path
import pytest
from python_project_deployment.exceptions import SecurityError
from python_project_deployment.security import (
find_binary,
sanitize_template_value,
set_secure_permissions,
validate_binary,
validate_filename,
validate_path_traversal,
)
class TestValidatePathTraversal:
"""Test path traversal validation."""
def test_valid_path_within_base(self):
"""Test that valid paths within base are accepted."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir)
subdir = base / "subdir"
subdir.mkdir()
result = validate_path_traversal(Path("subdir"), base)
assert result == subdir.resolve()
def test_reject_parent_directory_traversal(self):
"""Test that .. paths are rejected."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir) / "project"
base.mkdir()
with pytest.raises(SecurityError) as exc_info:
validate_path_traversal(Path(".."), base)
assert "Path traversal attempt detected" in str(exc_info.value)
def test_reject_absolute_path_escape(self):
"""Test that absolute paths outside base are rejected."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir) / "project"
base.mkdir()
with pytest.raises(SecurityError):
validate_path_traversal(Path("/etc/passwd"), base)
def test_reject_symlink_escape(self):
"""Test that symlinks pointing outside base are rejected."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir) / "project"
base.mkdir()
# Create a symlink pointing outside
link = base / "escape"
target = Path(tmpdir) / "outside"
target.mkdir()
try:
link.symlink_to(target)
with pytest.raises(SecurityError):
validate_path_traversal(link, base)
except OSError:
# Skip test if symlinking not supported
pytest.skip("Symlinks not supported on this platform")
def test_nested_path_within_base(self):
"""Test that nested paths within base are accepted."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir)
nested = base / "a" / "b" / "c"
nested.mkdir(parents=True)
result = validate_path_traversal(Path("a/b/c"), base)
assert result == nested.resolve()
class TestSanitizeTemplateValue:
"""Test template value sanitization."""
def test_valid_string_unchanged(self):
"""Test that valid strings pass through unchanged."""
value = "valid_package_name"
assert sanitize_template_value(value) == value
def test_remove_control_characters(self):
"""Test that control characters are removed."""
value = "test\x01\x02\x03value"
result = sanitize_template_value(value)
assert result == "testvalue"
def test_preserve_whitespace(self):
"""Test that tabs, newlines, and spaces are preserved."""
value = "test\ttab\nnewline\rreturn space"
result = sanitize_template_value(value)
assert "\t" in result
assert "\n" in result
assert "\r" in result
assert " " in result
def test_reject_null_bytes(self):
"""Test that null bytes raise SecurityError."""
value = "test\x00null"
with pytest.raises(SecurityError) as exc_info:
sanitize_template_value(value)
assert "Null bytes not allowed" in str(exc_info.value)
def test_reject_excessive_length(self):
"""Test that values exceeding max_length raise SecurityError."""
value = "a" * 1001
with pytest.raises(SecurityError) as exc_info:
sanitize_template_value(value)
assert "exceeds maximum length" in str(exc_info.value)
def test_custom_max_length(self):
"""Test custom max_length parameter."""
value = "a" * 50
# Should pass with default length
sanitize_template_value(value)
# Should fail with custom short length
with pytest.raises(SecurityError):
sanitize_template_value(value, max_length=10)
def test_reject_non_string_type(self):
"""Test that non-string values raise SecurityError."""
with pytest.raises(SecurityError) as exc_info:
sanitize_template_value(123) # type: ignore
assert "must be string" in str(exc_info.value)
class TestValidateBinary:
"""Test binary validation."""
def test_valid_binary(self):
"""Test that valid binaries pass validation."""
# Use a system binary that should exist
python_path = Path("/usr/bin/python3")
if python_path.exists():
result = validate_binary(python_path, "python3")
assert result is True
else:
pytest.skip("python3 not found at /usr/bin/python3")
def test_reject_nonexistent_binary(self):
"""Test that nonexistent binaries raise SecurityError."""
fake_path = Path("/nonexistent/binary")
with pytest.raises(SecurityError) as exc_info:
validate_binary(fake_path, "binary")
assert "Binary not found" in str(exc_info.value)
def test_reject_directory_as_binary(self):
"""Test that directories raise SecurityError."""
with tempfile.TemporaryDirectory() as tmpdir:
dir_path = Path(tmpdir)
with pytest.raises(SecurityError) as exc_info:
validate_binary(dir_path, "test")
assert "not a file" in str(exc_info.value)
def test_reject_non_executable(self):
"""Test that non-executable files raise SecurityError."""
with tempfile.TemporaryDirectory() as tmpdir:
file_path = Path(tmpdir) / "test"
file_path.touch()
# Make it non-executable
file_path.chmod(0o644)
with pytest.raises(SecurityError) as exc_info:
validate_binary(file_path, "test")
assert "not executable" in str(exc_info.value)
def test_reject_name_mismatch(self):
"""Test that name mismatches raise SecurityError."""
# Use ls as example - it exists and is executable
ls_path = Path("/bin/ls")
if ls_path.exists():
with pytest.raises(SecurityError) as exc_info:
validate_binary(ls_path, "wrong_name")
assert "name mismatch" in str(exc_info.value)
else:
pytest.skip("/bin/ls not found")
class TestFindBinary:
"""Test finding binaries in PATH."""
def test_find_existing_binary(self):
"""Test finding an existing binary."""
# Python should always be available
try:
result = find_binary("python3")
assert result.exists()
assert result.is_file()
assert result.name == "python3"
except SecurityError:
pytest.skip("python3 not found in PATH")
def test_reject_nonexistent_binary(self):
"""Test that nonexistent binaries raise SecurityError."""
with pytest.raises(SecurityError) as exc_info:
find_binary("nonexistent_binary_12345")
assert "not found in PATH" in str(exc_info.value)
class TestSetSecurePermissions:
"""Test setting secure file permissions."""
def test_set_file_permissions(self):
"""Test setting permissions on a file."""
with tempfile.TemporaryDirectory() as tmpdir:
file_path = Path(tmpdir) / "test.txt"
file_path.touch()
set_secure_permissions(file_path, is_directory=False)
# Check permissions: 0o644 (rw-r--r--)
mode = file_path.stat().st_mode
assert mode & stat.S_IRUSR # Owner read
assert mode & stat.S_IWUSR # Owner write
assert not (mode & stat.S_IXUSR) # Owner no execute
assert mode & stat.S_IRGRP # Group read
assert not (mode & stat.S_IWGRP) # Group no write
assert mode & stat.S_IROTH # Other read
assert not (mode & stat.S_IWOTH) # Other no write
def test_set_directory_permissions(self):
"""Test setting permissions on a directory."""
with tempfile.TemporaryDirectory() as tmpdir:
dir_path = Path(tmpdir) / "testdir"
dir_path.mkdir()
set_secure_permissions(dir_path, is_directory=True)
# Check permissions: 0o755 (rwxr-xr-x)
mode = dir_path.stat().st_mode
assert mode & stat.S_IRUSR # Owner read
assert mode & stat.S_IWUSR # Owner write
assert mode & stat.S_IXUSR # Owner execute
assert mode & stat.S_IRGRP # Group read
assert not (mode & stat.S_IWGRP) # Group no write
assert mode & stat.S_IXGRP # Group execute
assert mode & stat.S_IROTH # Other read
assert not (mode & stat.S_IWOTH) # Other no write
assert mode & stat.S_IXOTH # Other execute
def test_error_on_nonexistent_path(self):
"""Test that setting permissions on nonexistent path raises error."""
fake_path = Path("/nonexistent/path")
with pytest.raises(SecurityError):
set_secure_permissions(fake_path)
class TestValidateFilename:
"""Test filename validation."""
def test_valid_filename(self):
"""Test that valid filenames are accepted."""
valid_names = [
"file.txt",
"my-file_123.py",
"README.md",
"a" * 255, # Max length
]
for name in valid_names:
result = validate_filename(name)
assert result == name
def test_reject_empty_filename(self):
"""Test that empty filenames raise SecurityError."""
with pytest.raises(SecurityError) as exc_info:
validate_filename("")
assert "cannot be empty" in str(exc_info.value)
def test_reject_null_bytes(self):
"""Test that filenames with null bytes raise SecurityError."""
with pytest.raises(SecurityError) as exc_info:
validate_filename("file\x00name")
assert "Null bytes not allowed" in str(exc_info.value)
def test_reject_path_separators(self):
"""Test that path separators raise SecurityError."""
with pytest.raises(SecurityError) as exc_info:
validate_filename("path/to/file")
assert "Path separators not allowed" in str(exc_info.value)
with pytest.raises(SecurityError):
validate_filename("path\\to\\file")
def test_reject_dangerous_names(self):
"""Test that dangerous names raise SecurityError."""
dangerous = [".", "..", "~"]
for name in dangerous:
with pytest.raises(SecurityError) as exc_info:
validate_filename(name)
assert "Dangerous filename" in str(exc_info.value)
def test_reject_excessive_length(self):
"""Test that filenames > 255 chars raise SecurityError."""
long_name = "a" * 256
with pytest.raises(SecurityError) as exc_info:
validate_filename(long_name)
assert "too long" in str(exc_info.value)