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:
Chuck
2026-09-28 11:22:24 -04:00
co-authored by Claude Opus 5.5
parent f7bdb24772
commit 5e695b7c39
6 changed files with 23 additions and 19 deletions
+3 -2
View File
@@ -1466,8 +1466,9 @@ def _run_calendar_registration(plugin_dir: Path, stdin_payload: str):
except subprocess.TimeoutExpired:
return None, 'Authentication timed out after 120s'
except OSError as e:
logger.error('Could not run calendar_registration.py', exc_info=True)
return None, 'Could not run the authentication script: %s' % describe_exception(e)
# The exception (a path, an errno) goes to the log, not the client.
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()):
line = line.strip()
@@ -142,7 +142,9 @@ def upload_plugin_asset():
for file, file_ext, file_size, file_content in accepted:
# Generate unique filename
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}"
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 (
Path, _CALENDAR_LIST_MAX_PAGES, _plugin_directory,
_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,
)
from src.config_manager_atomic import atomic_write_text
@@ -158,29 +158,28 @@ def list_calendar_calendars():
}), 400
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 googleapiclient.discovery import build as build_google_service
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({
'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 '
"the calendar plugin's requirements.txt. (%s)"
% describe_exception(e))
"the calendar plugin's requirements.txt.")
}), 500
with open(token_file, 'rb') as handle:
# Written only by this plugin's own OAuth flow, into its own
# 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:
creds.refresh(GoogleRequest())
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)
if not creds or not creds.valid:
@@ -435,7 +435,8 @@ def save_plugin_config():
)
except Exception:
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(
ErrorCode.CONFIG_SAVE_FAILED,
"Failed to save secrets configuration; see logs for details",
@@ -738,7 +738,7 @@ def get_github_auth_status():
return jsonify({
'status': 'success',
'data': {
'token_status': 'none',
'token_status': 'none', # nosec B105 - a status label # nosemgrep
'authenticated': False,
'rate_limit': 60,
'message': 'No GitHub token configured',
@@ -753,7 +753,7 @@ def get_github_auth_status():
return jsonify({
'status': 'success',
'data': {
'token_status': 'valid',
'token_status': 'valid', # nosec B105 - a status label # nosemgrep
'authenticated': True,
'rate_limit': 5000,
'message': 'GitHub API authenticated',
@@ -764,7 +764,7 @@ def get_github_auth_status():
return jsonify({
'status': 'success',
'data': {
'token_status': 'invalid',
'token_status': 'invalid', # nosec B105 - a status label # nosemgrep
'authenticated': False,
'rate_limit': 60,
'message': f'GitHub token is invalid: {error_message}' if error_message else 'GitHub token is invalid',