Files
Fast-Whisper-MCP-Server/tests/test_input_validation.py
Alihan fb1e5dceba Upgrade to PyTorch 2.6.0 and enhance GPU reset script with Ollama management
- 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>
2025-10-27 23:01:22 +03:00

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"])