mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(web): keep exception text out of calendar responses; annotate moved code
The split made scanners report existing findings in the moved code as new: - CodeQL: the calendar auth and calendar-list routes returned exception text (redacted, but still derived from the exception). Both now log the exception and return a fixed message pointing at the log. - MD5 in the asset upload only makes a filename unique: usedforsecurity=False. - pickle reads/writes the calendar plugin's own OAuth token (as before): annotated. Token-status labels and a log line naming the secrets path are false positives: annotated with the repo's nosec/nosemgrep convention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -402,8 +402,8 @@ class TestDiagnosticsAreRedacted:
|
|||||||
tmp_path,
|
tmp_path,
|
||||||
monkeypatch):
|
monkeypatch):
|
||||||
# OSError from the spawn carries the interpreter path and whatever the
|
# OSError from the spawn carries the interpreter path and whatever the
|
||||||
# OS chose to say; it reaches the client through the redactor like
|
# OS chose to say; none of it reaches the client, which is pointed at
|
||||||
# everything else.
|
# the log instead.
|
||||||
script = tmp_path / 'calendar_registration.py'
|
script = tmp_path / 'calendar_registration.py'
|
||||||
script.write_text('', encoding='utf-8')
|
script.write_text('', encoding='utf-8')
|
||||||
|
|
||||||
@@ -414,7 +414,8 @@ class TestDiagnosticsAreRedacted:
|
|||||||
payload, error = mod._run_calendar_registration(tmp_path, '')
|
payload, error = mod._run_calendar_registration(tmp_path, '')
|
||||||
assert payload is None
|
assert payload is None
|
||||||
assert 'abcd1234' not in error, error
|
assert 'abcd1234' not in error, error
|
||||||
assert 'OSError' in error, error
|
assert 'Exec format' not in error, error
|
||||||
|
assert 'log' in error, error
|
||||||
|
|
||||||
def test_a_missing_google_library_is_reported_without_raw_exception_text(
|
def test_a_missing_google_library_is_reported_without_raw_exception_text(
|
||||||
self, client, monkeypatch):
|
self, client, monkeypatch):
|
||||||
|
|||||||
@@ -1466,8 +1466,9 @@ def _run_calendar_registration(plugin_dir: Path, stdin_payload: str):
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return None, 'Authentication timed out after 120s'
|
return None, 'Authentication timed out after 120s'
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error('Could not run calendar_registration.py', exc_info=True)
|
# The exception (a path, an errno) goes to the log, not the client.
|
||||||
return None, 'Could not run the authentication script: %s' % describe_exception(e)
|
logger.error('Could not run calendar_registration.py: %s', e, exc_info=True)
|
||||||
|
return None, 'Could not run the authentication script; see the web service log'
|
||||||
|
|
||||||
for line in reversed((result.stdout or '').splitlines()):
|
for line in reversed((result.stdout or '').splitlines()):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ def upload_plugin_asset():
|
|||||||
for file, file_ext, file_size, file_content in accepted:
|
for file, file_ext, file_size, file_content in accepted:
|
||||||
# Generate unique filename
|
# Generate unique filename
|
||||||
timestamp = int(_pkg.time.time())
|
timestamp = int(_pkg.time.time())
|
||||||
file_hash = hashlib.md5(file_content + file.filename.encode()).hexdigest()[:8]
|
# Only makes the filename unique; nothing is verified with it.
|
||||||
|
file_hash = hashlib.md5(file_content + file.filename.encode(),
|
||||||
|
usedforsecurity=False).hexdigest()[:8]
|
||||||
safe_filename = f"image_{timestamp}_{file_hash}{file_ext}"
|
safe_filename = f"image_{timestamp}_{file_hash}{file_ext}"
|
||||||
file_path = assets_dir / safe_filename
|
file_path = assets_dir / safe_filename
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ so their endpoint names do not depend on which module they live in.
|
|||||||
from web_interface.blueprints.api_v3 import (
|
from web_interface.blueprints.api_v3 import (
|
||||||
Path, _CALENDAR_LIST_MAX_PAGES, _plugin_directory,
|
Path, _CALENDAR_LIST_MAX_PAGES, _plugin_directory,
|
||||||
_prune_credential_backups, _run_calendar_registration, api_v3,
|
_prune_credential_backups, _run_calendar_registration, api_v3,
|
||||||
describe_exception, json, jsonify, logger, os, redact_text, request,
|
json, jsonify, logger, os, redact_text, request,
|
||||||
shutil,
|
shutil,
|
||||||
)
|
)
|
||||||
from src.config_manager_atomic import atomic_write_text
|
from src.config_manager_atomic import atomic_write_text
|
||||||
@@ -158,29 +158,28 @@ def list_calendar_calendars():
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pickle
|
import pickle # nosec B403 - reads only the plugin's own OAuth token # nosemgrep
|
||||||
from google.auth.transport.requests import Request as GoogleRequest
|
from google.auth.transport.requests import Request as GoogleRequest
|
||||||
from googleapiclient.discovery import build as build_google_service
|
from googleapiclient.discovery import build as build_google_service
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
|
# Which module is missing goes to the log; the exception itself (it
|
||||||
|
# can quote a path) doesn't go to the client.
|
||||||
|
logger.warning('Calendar picker: Google API libraries missing: %s', e)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
# The name of the missing module is the whole diagnosis, but it
|
|
||||||
# arrives as an exception, so it goes through the redactor like
|
|
||||||
# any other -- an ImportError can quote a path.
|
|
||||||
'message': ('The Google API libraries are not installed. Install '
|
'message': ('The Google API libraries are not installed. Install '
|
||||||
"the calendar plugin's requirements.txt. (%s)"
|
"the calendar plugin's requirements.txt.")
|
||||||
% describe_exception(e))
|
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
with open(token_file, 'rb') as handle:
|
with open(token_file, 'rb') as handle:
|
||||||
# Written only by this plugin's own OAuth flow, into its own
|
# Written only by this plugin's own OAuth flow, into its own
|
||||||
# directory, and read here exactly as the plugin itself reads it.
|
# directory, and read here exactly as the plugin itself reads it.
|
||||||
creds = pickle.load(handle) # nosec B301 - locally generated token
|
creds = pickle.load(handle) # nosec B301 - locally generated token # nosemgrep
|
||||||
|
|
||||||
if creds and creds.expired and creds.refresh_token:
|
if creds and creds.expired and creds.refresh_token:
|
||||||
creds.refresh(GoogleRequest())
|
creds.refresh(GoogleRequest())
|
||||||
with open(token_file, 'wb') as handle:
|
with open(token_file, 'wb') as handle:
|
||||||
pickle.dump(creds, handle)
|
pickle.dump(creds, handle) # nosec B301 - same token, same format # nosemgrep
|
||||||
os.chmod(token_file, 0o600)
|
os.chmod(token_file, 0o600)
|
||||||
|
|
||||||
if not creds or not creds.valid:
|
if not creds or not creds.valid:
|
||||||
|
|||||||
@@ -435,7 +435,8 @@ def save_plugin_config():
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
secrets_path = api_v3.config_manager.secrets_path
|
secrets_path = api_v3.config_manager.secrets_path
|
||||||
logger.error("Error saving secrets config for %s (path=%s)", plugin_id, secrets_path, exc_info=True)
|
# Logs the file path, not any secret value.
|
||||||
|
logger.error("Error saving secrets config for %s (path=%s)", plugin_id, secrets_path, exc_info=True) # nosemgrep
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCode.CONFIG_SAVE_FAILED,
|
ErrorCode.CONFIG_SAVE_FAILED,
|
||||||
"Failed to save secrets configuration; see logs for details",
|
"Failed to save secrets configuration; see logs for details",
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ def get_github_auth_status():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'data': {
|
'data': {
|
||||||
'token_status': 'none',
|
'token_status': 'none', # nosec B105 - a status label # nosemgrep
|
||||||
'authenticated': False,
|
'authenticated': False,
|
||||||
'rate_limit': 60,
|
'rate_limit': 60,
|
||||||
'message': 'No GitHub token configured',
|
'message': 'No GitHub token configured',
|
||||||
@@ -753,7 +753,7 @@ def get_github_auth_status():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'data': {
|
'data': {
|
||||||
'token_status': 'valid',
|
'token_status': 'valid', # nosec B105 - a status label # nosemgrep
|
||||||
'authenticated': True,
|
'authenticated': True,
|
||||||
'rate_limit': 5000,
|
'rate_limit': 5000,
|
||||||
'message': 'GitHub API authenticated',
|
'message': 'GitHub API authenticated',
|
||||||
@@ -764,7 +764,7 @@ def get_github_auth_status():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'data': {
|
'data': {
|
||||||
'token_status': 'invalid',
|
'token_status': 'invalid', # nosec B105 - a status label # nosemgrep
|
||||||
'authenticated': False,
|
'authenticated': False,
|
||||||
'rate_limit': 60,
|
'rate_limit': 60,
|
||||||
'message': f'GitHub token is invalid: {error_message}' if error_message else 'GitHub token is invalid',
|
'message': f'GitHub token is invalid: {error_message}' if error_message else 'GitHub token is invalid',
|
||||||
|
|||||||
Reference in New Issue
Block a user