- Upgrade PyTorch and torchaudio to 2.6.0 with CUDA 12.4 support - Update GPU reset script to gracefully stop/start Ollama via supervisorctl - Add Docker Compose configuration for both API and MCP server modes - Implement comprehensive Docker entrypoint for multi-mode deployment - Add GPU health check cleanup to prevent memory leaks - Fix transcription memory management with proper resource cleanup - Add filename security validation to prevent path traversal attacks - Include .dockerignore for optimized Docker builds - Remove deprecated supervisor configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
282 lines
9.7 KiB
Python
282 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for input validation module, specifically filename validation.
|
|
|
|
Tests the security-critical validate_filename_safe() function to ensure
|
|
it correctly blocks path traversal attacks while allowing legitimate filenames.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import pytest
|
|
|
|
# Add src to path (go up one level from tests/ to root)
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
from src.utils.input_validation import (
|
|
validate_filename_safe,
|
|
ValidationError,
|
|
PathTraversalError,
|
|
InvalidFileTypeError,
|
|
ALLOWED_AUDIO_EXTENSIONS
|
|
)
|
|
|
|
|
|
class TestValidFilenameSafe:
|
|
"""Test validate_filename_safe() function with various inputs."""
|
|
|
|
def test_simple_valid_filenames(self):
|
|
"""Test that simple, valid filenames are accepted."""
|
|
valid_names = [
|
|
"audio.m4a",
|
|
"song.wav",
|
|
"podcast.mp3",
|
|
"recording.flac",
|
|
"music.ogg",
|
|
"voice.aac",
|
|
]
|
|
|
|
for filename in valid_names:
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept: {filename}"
|
|
|
|
def test_filenames_with_ellipsis(self):
|
|
"""Test filenames with ellipsis (multiple dots) are accepted."""
|
|
# This is the key test case from the bug report
|
|
ellipsis_names = [
|
|
"audio...mp3",
|
|
"This is... a test.m4a",
|
|
"Part 1... Part 2.wav",
|
|
"Wait... what.m4a",
|
|
"video...multiple...dots.mp3",
|
|
"This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a", # Bug report case
|
|
]
|
|
|
|
for filename in ellipsis_names:
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept filename with ellipsis: {filename}"
|
|
|
|
def test_filenames_with_special_chars(self):
|
|
"""Test filenames with various special characters."""
|
|
special_char_names = [
|
|
"My-Video_2024.m4a",
|
|
"song (remix).m4a",
|
|
"audio [final].wav",
|
|
"test file with spaces.mp3",
|
|
"file-name_with-symbols.flac",
|
|
]
|
|
|
|
for filename in special_char_names:
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept: {filename}"
|
|
|
|
def test_multiple_extensions(self):
|
|
"""Test filenames that look like they have multiple extensions."""
|
|
multi_ext_names = [
|
|
"backup.tar.gz.mp3", # .mp3 is valid
|
|
"file.old.wav", # .wav is valid
|
|
"audio.2024.m4a", # .m4a is valid
|
|
]
|
|
|
|
for filename in multi_ext_names:
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept: {filename}"
|
|
|
|
def test_path_traversal_attempts(self):
|
|
"""Test that path traversal attempts are rejected."""
|
|
dangerous_names = [
|
|
"../../../etc/passwd",
|
|
"../../secrets.txt",
|
|
"../file.mp4",
|
|
"dir/../file.mp4",
|
|
"file/../../etc/passwd",
|
|
]
|
|
|
|
for filename in dangerous_names:
|
|
with pytest.raises(PathTraversalError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "path" in str(exc_info.value).lower(), f"Should reject path traversal: {filename}"
|
|
|
|
def test_absolute_paths(self):
|
|
"""Test that absolute paths are rejected."""
|
|
absolute_paths = [
|
|
"/etc/passwd",
|
|
"/tmp/file.mp4",
|
|
"/home/user/audio.wav",
|
|
"C:\\Windows\\System32\\file.mp3", # Windows path
|
|
"\\\\server\\share\\file.m4a", # UNC path
|
|
]
|
|
|
|
for filename in absolute_paths:
|
|
with pytest.raises(PathTraversalError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "path" in str(exc_info.value).lower(), f"Should reject absolute path: {filename}"
|
|
|
|
def test_path_separators(self):
|
|
"""Test that filenames with path separators are rejected."""
|
|
paths_with_separators = [
|
|
"dir/file.mp4",
|
|
"folder\\file.wav",
|
|
"path/to/audio.m4a",
|
|
"a/b/c/d.mp3",
|
|
]
|
|
|
|
for filename in paths_with_separators:
|
|
with pytest.raises(PathTraversalError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "separator" in str(exc_info.value).lower() or "path" in str(exc_info.value).lower(), \
|
|
f"Should reject path with separators: {filename}"
|
|
|
|
def test_null_bytes(self):
|
|
"""Test that filenames with null bytes are rejected."""
|
|
null_byte_names = [
|
|
"file\x00.mp4",
|
|
"\x00malicious.wav",
|
|
"audio\x00evil.m4a",
|
|
]
|
|
|
|
for filename in null_byte_names:
|
|
with pytest.raises(PathTraversalError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "null" in str(exc_info.value).lower(), f"Should reject null bytes: {repr(filename)}"
|
|
|
|
def test_empty_filename(self):
|
|
"""Test that empty filename is rejected."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
validate_filename_safe("")
|
|
assert "empty" in str(exc_info.value).lower()
|
|
|
|
def test_no_extension(self):
|
|
"""Test that filenames without extensions are rejected."""
|
|
no_ext_names = [
|
|
"filename",
|
|
"noextension",
|
|
]
|
|
|
|
for filename in no_ext_names:
|
|
with pytest.raises(InvalidFileTypeError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "extension" in str(exc_info.value).lower(), f"Should reject no extension: {filename}"
|
|
|
|
def test_invalid_extensions(self):
|
|
"""Test that unsupported file extensions are rejected."""
|
|
invalid_ext_names = [
|
|
"document.pdf",
|
|
"image.png",
|
|
"video.avi",
|
|
"script.sh",
|
|
"executable.exe",
|
|
"text.txt",
|
|
]
|
|
|
|
for filename in invalid_ext_names:
|
|
with pytest.raises(InvalidFileTypeError) as exc_info:
|
|
validate_filename_safe(filename)
|
|
assert "unsupported" in str(exc_info.value).lower() or "format" in str(exc_info.value).lower(), \
|
|
f"Should reject invalid extension: {filename}"
|
|
|
|
def test_case_insensitive_extensions(self):
|
|
"""Test that file extensions are case-insensitive."""
|
|
case_variations = [
|
|
"audio.MP3",
|
|
"sound.WAV",
|
|
"music.M4A",
|
|
"podcast.FLAC",
|
|
"voice.AAC",
|
|
]
|
|
|
|
for filename in case_variations:
|
|
# Should not raise exception
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept case variation: {filename}"
|
|
|
|
def test_edge_cases(self):
|
|
"""Test various edge cases."""
|
|
# Just dots (but with valid extension) - should pass
|
|
assert validate_filename_safe("...mp3") == "...mp3"
|
|
assert validate_filename_safe("....wav") == "....wav"
|
|
|
|
# Filenames starting with dot (hidden files on Unix)
|
|
assert validate_filename_safe(".hidden.m4a") == ".hidden.m4a"
|
|
|
|
# Very long filename (but valid)
|
|
long_name = "a" * 200 + ".mp3"
|
|
assert validate_filename_safe(long_name) == long_name
|
|
|
|
def test_allowed_extensions_comprehensive(self):
|
|
"""Test all allowed extensions from ALLOWED_AUDIO_EXTENSIONS."""
|
|
for ext in ALLOWED_AUDIO_EXTENSIONS:
|
|
filename = f"test{ext}"
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should accept allowed extension: {ext}"
|
|
|
|
|
|
class TestBugReportCase:
|
|
"""Specific test for the bug report case."""
|
|
|
|
def test_bug_report_filename(self):
|
|
"""
|
|
Test the exact filename from the bug report that was failing.
|
|
|
|
Bug: "This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a"
|
|
was being rejected due to "..." being parsed as ".."
|
|
"""
|
|
filename = "This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a"
|
|
|
|
# Should NOT raise any exception
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename
|
|
|
|
def test_various_ellipsis_patterns(self):
|
|
"""Test various ellipsis patterns that should all be accepted."""
|
|
patterns = [
|
|
"...", # Three dots
|
|
"....", # Four dots
|
|
".....", # Five dots
|
|
"file...end.mp3",
|
|
"start...middle...end.wav",
|
|
]
|
|
|
|
for pattern in patterns:
|
|
if not pattern.endswith(tuple(f"{ext}" for ext in ALLOWED_AUDIO_EXTENSIONS)):
|
|
pattern += ".mp3" # Add valid extension
|
|
result = validate_filename_safe(pattern)
|
|
assert result == pattern
|
|
|
|
|
|
class TestSecurityBoundary:
|
|
"""Test the security boundary between safe and dangerous filenames."""
|
|
|
|
def test_just_two_dots_vs_path_separator(self):
|
|
"""
|
|
Test the critical distinction:
|
|
- "file..mp3" (two dots in filename) = SAFE
|
|
- "../file.mp3" (two dots as path component) = DANGEROUS
|
|
"""
|
|
# Safe: dots within filename
|
|
safe_filenames = [
|
|
"file..mp3",
|
|
"..file.mp3",
|
|
"file...mp3",
|
|
"f..i..l..e.mp3",
|
|
]
|
|
|
|
for filename in safe_filenames:
|
|
result = validate_filename_safe(filename)
|
|
assert result == filename, f"Should be safe: {filename}"
|
|
|
|
# Dangerous: dots as directory reference
|
|
dangerous_filenames = [
|
|
"../file.mp3",
|
|
"../../file.mp3",
|
|
"dir/../file.mp3",
|
|
]
|
|
|
|
for filename in dangerous_filenames:
|
|
with pytest.raises(PathTraversalError):
|
|
validate_filename_safe(filename)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|