Skip to content

Commit 652c9a5

Browse files
cfsmp3claude
andcommitted
fix: Remove server-side binary execution for security
Per maintainer feedback, we should not execute untrusted binaries on the platform server as this could allow malicious code to infect the system. Changes: - Remove _verify_binary_commit function that ran binaries on the server - Remove subprocess import (no longer needed) - Update start_test to just log expected commit SHA - Binary verification now happens only in the sandboxed GCP VMs via runCI scripts - The runCI scripts (already added) log ccextractor --version for audit trail The VM-based approach is secure because: - VMs are isolated and disposable - Each test runs in a fresh VM that gets destroyed after - No way for malicious code to escape to the platform server 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 423ae18 commit 652c9a5

File tree

2 files changed

+11
-196
lines changed

2 files changed

+11
-196
lines changed

mod_ci/controllers.py

Lines changed: 6 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
import os
77
import re
88
import shutil
9-
import subprocess
109
import time
1110
import zipfile
1211
from collections import defaultdict
1312
from pathlib import Path
14-
from typing import Any, Dict, Optional, Tuple
13+
from typing import Any, Dict, Optional
1514

1615
import googleapiclient.discovery
1716
import requests
@@ -342,61 +341,6 @@ def mark_test_failed(db, test, repository, message: str) -> None:
342341
log.error(f"Failed to mark test {test.id} as failed: {e}")
343342

344343

345-
def _verify_binary_commit(binary_path: str, expected_commit: str, log) -> Tuple[bool, str]:
346-
"""
347-
Verify that the binary's embedded commit SHA matches the expected commit.
348-
349-
Runs the binary with --version and parses the output to extract the Git commit.
350-
This ensures we're testing the correct binary and prevents race conditions.
351-
352-
:param binary_path: Path to the ccextractor binary
353-
:type binary_path: str
354-
:param expected_commit: The expected Git commit SHA
355-
:type expected_commit: str
356-
:param log: Logger instance
357-
:return: Tuple of (success, message)
358-
:rtype: tuple[bool, str]
359-
"""
360-
try:
361-
result = subprocess.run(
362-
[binary_path, '--version'],
363-
capture_output=True,
364-
text=True,
365-
timeout=30
366-
)
367-
368-
# Parse output for Git commit line
369-
# Example output: " Git commit: 7a9acb7bd2cb1ae492b4a67d14c9e1687a0f607f"
370-
for line in result.stdout.split('\n'):
371-
if 'Git commit:' in line:
372-
binary_commit = line.split('Git commit:')[1].strip()
373-
if binary_commit == expected_commit:
374-
log.info(f"Binary commit verified: {binary_commit}")
375-
return True, f"Binary commit verified: {binary_commit}"
376-
else:
377-
error_msg = (f"Binary commit mismatch! Expected {expected_commit}, "
378-
f"but binary reports {binary_commit}")
379-
log.error(error_msg)
380-
return False, error_msg
381-
382-
error_msg = f"Could not find Git commit in binary --version output: {result.stdout[:500]}"
383-
log.error(error_msg)
384-
return False, error_msg
385-
386-
except subprocess.TimeoutExpired:
387-
error_msg = "Binary --version timed out after 30 seconds"
388-
log.error(error_msg)
389-
return False, error_msg
390-
except FileNotFoundError:
391-
error_msg = f"Binary not found at {binary_path}"
392-
log.error(error_msg)
393-
return False, error_msg
394-
except Exception as e:
395-
error_msg = f"Error verifying binary commit: {e}"
396-
log.error(error_msg)
397-
return False, error_msg
398-
399-
400344
def start_test(compute, app, db, repository: Repository.Repository, test, bot_token) -> None:
401345
"""
402346
Start a VM instance and run the tests.
@@ -563,25 +507,11 @@ def start_test(compute, app, db, repository: Repository.Repository, test, bot_to
563507
with zipfile.ZipFile(zip_path, 'r') as artifact_zip:
564508
artifact_zip.extractall(base_folder)
565509

566-
# Verify the binary's embedded commit SHA matches expected commit
567-
# This prevents race conditions where an old artifact might be used
568-
if test.platform == TestPlatform.linux:
569-
binary_path = os.path.join(base_folder, 'ccextractor')
570-
# Make binary executable - this is safe because:
571-
# 1. The binary comes from GitHub's official artifact storage
572-
# 2. The path is in our controlled temp directory (base_folder)
573-
# 3. We verify the binary's commit SHA immediately after
574-
os.chmod(binary_path, 0o755) # NOSONAR - Safe: trusted source, controlled path
575-
verified, verify_msg = _verify_binary_commit(binary_path, test.commit, log)
576-
if not verified:
577-
mark_test_failed(db, test, repository, verify_msg)
578-
return
579-
else:
580-
# Windows binaries can't be verified on Linux server
581-
# The binary name is ccextractorwinfull.exe
582-
# Log expected commit for manual verification from VM logs
583-
log.info(f"Windows test {test.id}: expected binary commit is {test.commit}")
584-
log.info(f"Binary version will be logged by runCI.bat on the Windows VM")
510+
# Log expected commit - actual verification happens in the VM via runCI scripts
511+
# The VM scripts log ccextractor --version output for audit trail
512+
# This avoids running untrusted binaries on the platform server
513+
log.info(f"Test {test.id} ({test.platform.value}): expected binary commit is {test.commit}")
514+
log.info(f"Binary version will be logged by runCI script on the {test.platform.value} VM")
585515

586516
artifact_saved = True
587517
break

tests/test_ci/test_controllers.py

Lines changed: 5 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from flask import g
77

88
from mod_auth.models import Role
9-
from mod_ci.controllers import (Workflow_builds, _verify_binary_commit,
10-
get_info_for_pr_comment, is_valid_commit_hash,
11-
progress_type_request, start_platforms)
9+
from mod_ci.controllers import (Workflow_builds, get_info_for_pr_comment,
10+
is_valid_commit_hash, progress_type_request,
11+
start_platforms)
1212
from mod_ci.models import BlockedUsers
1313
from mod_customized.models import CustomizedTest
1414
from mod_home.models import CCExtractorVersion, GeneralData
@@ -219,11 +219,8 @@ def test_cron_job_empty_token(self, mock_log):
219219
@mock.patch('mod_ci.controllers.g')
220220
@mock.patch('mod_ci.controllers.TestProgress')
221221
@mock.patch('mod_ci.controllers.GcpInstance')
222-
@mock.patch('mod_ci.controllers._verify_binary_commit')
223-
@mock.patch('os.chmod')
224-
def test_start_test(self, mock_chmod, mock_verify_binary_commit, mock_gcp_instance,
225-
mock_test_progress, mock_g, mock_open_file,
226-
mock_create_instance, mock_wait_for_operation):
222+
def test_start_test(self, mock_gcp_instance, mock_test_progress, mock_g,
223+
mock_open_file, mock_create_instance, mock_wait_for_operation):
227224
"""Test start_test function."""
228225
import zipfile
229226

@@ -269,9 +266,6 @@ def extractall(*args, **kwargs):
269266
mock_query = create_mock_db_query(mock_g)
270267
mock_query.c.got = MagicMock()
271268

272-
# Mock binary verification to succeed
273-
mock_verify_binary_commit.return_value = (True, "Binary commit verified")
274-
275269
# Test when gcp create instance fails
276270
mock_wait_for_operation.return_value = 'error occurred'
277271
start_test(mock.ANY, self.app, mock_g.db, repository, test, mock.ANY)
@@ -286,17 +280,6 @@ def extractall(*args, **kwargs):
286280
mock_create_instance.assert_called_once()
287281
mock_wait_for_operation.assert_called_once()
288282

289-
# Reset mocks for next test
290-
mock_g.db.commit.reset_mock()
291-
mock_create_instance.reset_mock()
292-
mock_wait_for_operation.reset_mock()
293-
294-
# Test when binary commit verification fails
295-
mock_verify_binary_commit.return_value = (False, "Binary commit mismatch!")
296-
start_test(mock.ANY, self.app, mock_g.db, repository, test, mock.ANY)
297-
# Should not proceed to create instance when verification fails
298-
mock_create_instance.assert_not_called()
299-
300283
@mock.patch('github.Github.get_repo')
301284
@mock.patch('mod_ci.controllers.start_test')
302285
@mock.patch('mod_ci.controllers.get_compute_service_object')
@@ -2381,101 +2364,3 @@ def generate_header(data, event, ci_key=None):
23812364
'utf-8'), g.github['ci_key'] if ci_key is None else ci_key)
23822365
headers = generate_git_api_header(event, sig)
23832366
return headers
2384-
2385-
2386-
class TestVerifyBinaryCommit(BaseTestCase):
2387-
"""Test the _verify_binary_commit function."""
2388-
2389-
@mock.patch('mod_ci.controllers.subprocess.run')
2390-
def test_verify_binary_commit_success(self, mock_run):
2391-
"""Test successful commit verification."""
2392-
mock_log = MagicMock()
2393-
expected_commit = "abc123def456"
2394-
2395-
# Mock successful --version output
2396-
mock_run.return_value = MagicMock(
2397-
stdout="CCExtractor detailed version info\n"
2398-
" Version: 0.96\n"
2399-
f" Git commit: {expected_commit}\n"
2400-
" Compilation date: 2025-12-23\n"
2401-
)
2402-
2403-
success, message = _verify_binary_commit("/path/to/ccextractor", expected_commit, mock_log)
2404-
2405-
self.assertTrue(success)
2406-
self.assertIn("verified", message)
2407-
mock_log.info.assert_called()
2408-
2409-
@mock.patch('mod_ci.controllers.subprocess.run')
2410-
def test_verify_binary_commit_mismatch(self, mock_run):
2411-
"""Test commit verification fails on mismatch."""
2412-
mock_log = MagicMock()
2413-
expected_commit = "abc123def456"
2414-
actual_commit = "different789"
2415-
2416-
mock_run.return_value = MagicMock(
2417-
stdout=f" Git commit: {actual_commit}\n"
2418-
)
2419-
2420-
success, message = _verify_binary_commit("/path/to/ccextractor", expected_commit, mock_log)
2421-
2422-
self.assertFalse(success)
2423-
self.assertIn("mismatch", message)
2424-
self.assertIn(expected_commit, message)
2425-
self.assertIn(actual_commit, message)
2426-
mock_log.error.assert_called()
2427-
2428-
@mock.patch('mod_ci.controllers.subprocess.run')
2429-
def test_verify_binary_commit_no_git_line(self, mock_run):
2430-
"""Test verification fails when Git commit line is missing."""
2431-
mock_log = MagicMock()
2432-
2433-
mock_run.return_value = MagicMock(
2434-
stdout="CCExtractor version info\n Version: 0.96\n"
2435-
)
2436-
2437-
success, message = _verify_binary_commit("/path/to/ccextractor", "abc123", mock_log)
2438-
2439-
self.assertFalse(success)
2440-
self.assertIn("Could not find Git commit", message)
2441-
mock_log.error.assert_called()
2442-
2443-
@mock.patch('mod_ci.controllers.subprocess.run')
2444-
def test_verify_binary_commit_timeout(self, mock_run):
2445-
"""Test verification handles timeout."""
2446-
import subprocess
2447-
mock_log = MagicMock()
2448-
2449-
mock_run.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=30)
2450-
2451-
success, message = _verify_binary_commit("/path/to/ccextractor", "abc123", mock_log)
2452-
2453-
self.assertFalse(success)
2454-
self.assertIn("timed out", message)
2455-
mock_log.error.assert_called()
2456-
2457-
@mock.patch('mod_ci.controllers.subprocess.run')
2458-
def test_verify_binary_commit_file_not_found(self, mock_run):
2459-
"""Test verification handles missing binary."""
2460-
mock_log = MagicMock()
2461-
2462-
mock_run.side_effect = FileNotFoundError()
2463-
2464-
success, message = _verify_binary_commit("/path/to/ccextractor", "abc123", mock_log)
2465-
2466-
self.assertFalse(success)
2467-
self.assertIn("not found", message)
2468-
mock_log.error.assert_called()
2469-
2470-
@mock.patch('mod_ci.controllers.subprocess.run')
2471-
def test_verify_binary_commit_generic_exception(self, mock_run):
2472-
"""Test verification handles unexpected exceptions."""
2473-
mock_log = MagicMock()
2474-
2475-
mock_run.side_effect = Exception("Unexpected error")
2476-
2477-
success, message = _verify_binary_commit("/path/to/ccextractor", "abc123", mock_log)
2478-
2479-
self.assertFalse(success)
2480-
self.assertIn("Error verifying", message)
2481-
mock_log.error.assert_called()

0 commit comments

Comments
 (0)