Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion field-logic/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,29 @@ def main():
filename = query.get('file', [''])[0]
timestamp = query.get('t', ['0'])[0]

# Validate timestamp
try:
float(timestamp)
except ValueError:
print("Error: Invalid timestamp")
return

if not filename:
print("Error: No filename provided")
return

file_path = os.path.join(MEDIA_DIR, filename)
# Security check: Prevent path traversal
base_dir = os.path.abspath(MEDIA_DIR)
file_path = os.path.abspath(os.path.join(base_dir, filename))

try:
if os.path.commonpath([base_dir, file_path]) != base_dir:
print("Error: Invalid filename (path traversal detected)")
return
except ValueError:
# Handles cases like different drives on Windows
print("Error: Invalid filename (path traversal detected)")
return

# 2. Launch VLC
# vlc "path" --start-time=X
Expand Down
89 changes: 89 additions & 0 deletions field-logic/test_launcher_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import unittest
from unittest.mock import patch, MagicMock
import os
import sys
import io

# Add current directory to path so we can import launcher
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

import launcher

class TestLauncherSecurity(unittest.TestCase):
@patch('subprocess.Popen')
def test_path_traversal_prevention(self, mock_popen):
# Mock configuration
launcher.MEDIA_DIR = '/tmp/media'
launcher.VLC_PATH = '/usr/bin/vlc'

filename = '../secret.txt'
uri = f'fieldlogic://open?file={filename}&t=0'

with patch.object(sys, 'argv', ['launcher.py', uri]):
with patch('sys.stdout', new=io.StringIO()) as fake_stdout:
launcher.main()
output = fake_stdout.getvalue()

self.assertFalse(mock_popen.called, "subprocess.Popen should not be called for path traversal attempt")
self.assertIn("Error: Invalid filename (path traversal detected)", output)

@patch('subprocess.Popen')
def test_absolute_path_prevention(self, mock_popen):
# Mock configuration
launcher.MEDIA_DIR = '/tmp/media'
launcher.VLC_PATH = '/usr/bin/vlc'

filename = '/etc/passwd'
uri = f'fieldlogic://open?file={filename}&t=0'

with patch.object(sys, 'argv', ['launcher.py', uri]):
with patch('sys.stdout', new=io.StringIO()) as fake_stdout:
launcher.main()
output = fake_stdout.getvalue()

self.assertFalse(mock_popen.called, "subprocess.Popen should not be called for absolute path attempt")
self.assertIn("Error: Invalid filename (path traversal detected)", output)

@patch('subprocess.Popen')
def test_invalid_timestamp(self, mock_popen):
# Mock configuration
launcher.MEDIA_DIR = '/tmp/media'
launcher.VLC_PATH = '/usr/bin/vlc'

filename = 'interview.webm'
uri = f'fieldlogic://open?file={filename}&t=not-a-number'

with patch.object(sys, 'argv', ['launcher.py', uri]):
with patch('sys.stdout', new=io.StringIO()) as fake_stdout:
launcher.main()
output = fake_stdout.getvalue()

self.assertFalse(mock_popen.called, "subprocess.Popen should not be called for invalid timestamp")
self.assertIn("Error: Invalid timestamp", output)

@patch('subprocess.Popen')
def test_valid_path(self, mock_popen):
# Mock configuration
launcher.MEDIA_DIR = '/tmp/media'
launcher.VLC_PATH = '/usr/bin/vlc'

filename = 'interview_01.webm'
uri = f'fieldlogic://open?file={filename}&t=123.45'

with patch.object(sys, 'argv', ['launcher.py', uri]):
with patch('sys.stdout', new=io.StringIO()) as fake_stdout:
launcher.main()
output = fake_stdout.getvalue()

self.assertTrue(mock_popen.called, "subprocess.Popen should be called for valid path")
args, _ = mock_popen.call_args
cmd = args[0]
file_path = cmd[1]
timestamp_arg = cmd[2]

expected_path = os.path.abspath(os.path.join('/tmp/media', filename))
self.assertEqual(file_path, expected_path)
self.assertEqual(timestamp_arg, "--start-time=123.45")

if __name__ == '__main__':
unittest.main()