Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb0f354
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
8ddd800
fix(security): scope tool error redaction to public boundary
seonghobae Sep 15, 2026
14af4b1
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
8e169e1
test(security): preserve focused redaction evidence
seonghobae Sep 15, 2026
5d035f3
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
685a2d1
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
2361b8c
test(security): restore focused public error redaction contract
seonghobae Sep 15, 2026
443c1f4
docs(security): restore tool error redaction boundary
seonghobae Sep 15, 2026
bb11aae
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
82b86fc
test(security): restore strong tool redaction contract
seonghobae Sep 15, 2026
c400133
docs(security): restore tool redaction traceability
seonghobae Sep 15, 2026
edc23f9
chore(stack): adopt #1695 owner lineage
seonghobae Sep 15, 2026
bdc6ea2
Merge parent 'feature/add-random-selection-tools' into fix-exception-…
seonghobae Sep 15, 2026
884abde
fix(security): preserve redaction contract while adopting selector pa…
seonghobae Sep 15, 2026
5bd37b7
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 15, 2026
672e60c
test(security): restore strong tool redaction regression
seonghobae Sep 16, 2026
4cd24c0
docs(security): restore tool redaction boundary evidence
seonghobae Sep 16, 2026
bfd9ce2
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 16, 2026
6573327
repair(security): adopt repaired selector parent without product drift
seonghobae Sep 16, 2026
a191c60
chore(stack): restack public-error redaction on repaired selector owner
seonghobae Sep 16, 2026
d617d98
보안 향상을 위한 예외 객체 문자열 보간 제거
seonghobae Sep 16, 2026
bb24087
restack(tool-errors): adopt repaired selector parent without source d…
seonghobae Sep 17, 2026
a57db93
chore(stack): restack public-error redaction on repaired selector owner
seonghobae Sep 17, 2026
72777db
chore(stack): adopt current selector parent without source change
seonghobae Sep 17, 2026
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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,7 @@
**Vulnerability:** The `_safe_filename` function in `backend/services/attachment_parser.py` used `pathlib.Path().name` to strip directory components from attachment filenames, but failed to normalize backslashes beforehand. This allowed attackers to use Windows-style path separators (e.g., `..\..\upload`) to bypass path validation on POSIX systems.
**Learning:** Checking for traversal sequences using `pathlib.Path().name` may leave the result vulnerable if the input path can contain Windows-style path separators but the program interprets it dynamically or decodes payloads using backslashes, because POSIX `pathlib` treats backslashes as valid filename characters, not separators.
**Prevention:** Always convert backslashes to forward slashes before parsing filenames using `pathlib.Path().name`.
## 2026-06-21 - Exception Leakage in Logging and Errors
**Vulnerability:** Raw exception objects were directly interpolated into log messages and raised errors using `f"{e}"`.
**Learning:** This practice can inadvertently leak sensitive internal details, such as file paths, database connection strings, or external service responses, to logs and API clients.
**Prevention:** Instead of string interpolation, use `exc_info=True` in logging calls to capture the full traceback safely, and use static or generic strings for raised exceptions to prevent leakage to end users.
2 changes: 1 addition & 1 deletion backend/api/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ async def send_email_endpoint(
except HTTPException:
raise
except Exception as e:
logger.error(f"Error sending email: {e}", exc_info=True)
logger.error("Error sending email", exc_info=True)
raise HTTPException(
status_code=500, detail="An internal error occurred while sending the email"
)
4 changes: 2 additions & 2 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ async def handler(params: Dict[str, Any]) -> Any:
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
raise ValueError(f"Webhook execution failed: {str(e)}")
raise ValueError("Webhook execution failed")

return handler

Expand Down Expand Up @@ -603,7 +603,7 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]:
)
}
except Exception as e:
raise ValueError(f"Invalid Base64 string: {e}")
raise ValueError("Invalid Base64 string")


registry.register(
Expand Down
8 changes: 4 additions & 4 deletions backend/import_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def import_eml_file(session, eml_file: Path) -> bool:
try:
parsed = parse_eml(eml_file)
except Exception as e:
logger.error(f"Failed to parse {eml_file}: {e}")
logger.error(f"Failed to parse {eml_file}", exc_info=True)
return False

existing = await session.execute(
Expand All @@ -56,7 +56,7 @@ async def import_eml_file(session, eml_file: Path) -> bool:
try:
body_emb = await generate_fixture_embedding(body_text)
except Exception as e:
logger.error(f"Failed to generate embedding for {eml_file}: {e}")
logger.error(f"Failed to generate embedding for {eml_file}", exc_info=True)
return False

thread_id = await assign_thread_id(
Expand Down Expand Up @@ -94,14 +94,14 @@ async def import_eml_file(session, eml_file: Path) -> bool:
)
)
except Exception as e:
logger.error(f"Failed to generate embedding for attachment {att['filename']}: {e}")
logger.error(f"Failed to generate embedding for attachment {att['filename']}", exc_info=True)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

session.add(email_obj)
try:
await session.commit()
except Exception as e:
await session.rollback()
logger.error(f"Failed to commit {eml_file}: {e}")
logger.error(f"Failed to commit {eml_file}", exc_info=True)
return False
logger.info(
f"Imported {eml_file.name} with {len(parsed.get('attachments', []))} attachments."
Expand Down
2 changes: 1 addition & 1 deletion backend/runner/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async def _listen_loop(self):
if websockets and isinstance(e, websockets.exceptions.ConnectionClosed):
logger.warning("Connection closed by remote gateway.")
else:
logger.warning(f"Connection loop ended: {e}")
logger.warning("Connection loop ended", exc_info=True)
self.is_connected = False

async def handle_message(self, message: str | bytes):
Expand Down
2 changes: 1 addition & 1 deletion backend/scripts/import_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def process_zip_file(zip_path: str | Path, session: AsyncSession):
try:
email_data = parse_eml(file_path)
except Exception as e:
logger.error(f"Failed to parse {file_path}: {e}")
logger.error(f"Failed to parse {file_path}", exc_info=True)
continue

chunks = chunk_text(email_data["body"])
Expand Down
2 changes: 1 addition & 1 deletion backend/services/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def extract_backup(zip_path: str | Path, output_dir: str | Path) -> list[Path]:
extracted_paths.append(target_path)

except (BadZipFile, FileNotFoundError) as e:
raise InvalidArchiveError(f"Failed to extract archive: {e}") from e
raise InvalidArchiveError("Failed to extract archive") from e

return extracted_paths

Expand Down
6 changes: 3 additions & 3 deletions backend/services/email_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ async def _create_connection(self, timeout: float | None):
) from exc
except OSError as exc:
raise SMTPConnectError(
f"Error connecting to {self._tls_server_hostname}: {exc}"
f"Error connecting to {self._tls_server_hostname}"
) from exc

self.protocol = protocol
Expand All @@ -484,7 +484,7 @@ async def _create_connection(self, timeout: float | None):
response = await protocol.read_response(timeout=timeout)
except SMTPServerDisconnected as exc:
raise SMTPConnectError(
f"Error connecting to {self._tls_server_hostname}: {exc}"
f"Error connecting to {self._tls_server_hostname}"
) from exc
except SMTPTimeoutError as exc:
raise SMTPConnectTimeoutError(
Expand Down Expand Up @@ -619,4 +619,4 @@ async def send_email(
except ValueError:
raise
except Exception as e:
raise Exception(f"Failed to send email: {e}")
raise Exception("Failed to send email")
2 changes: 1 addition & 1 deletion backend/services/email_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def parse_eml(file_path: str | Path) -> EmailData:
with open(file_path, "rb") as f:
msg = message_from_binary_file(f, policy=policy.default)
except OSError as e:
raise EmailParseError(f"Failed to read file {file_path}: {e}") from e
raise EmailParseError(f"Failed to read file {file_path}") from e

return _message_to_email_data(msg)

Expand Down
2 changes: 1 addition & 1 deletion backend/services/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ async def generate_embeddings(
)
return [data.embedding for data in response.data]
except openai.OpenAIError as e:
raise EmbeddingGenerationError(f"Failed to generate embeddings: {str(e)}")
raise EmbeddingGenerationError("Failed to generate embeddings")
finally:
await client.close()
2 changes: 1 addition & 1 deletion backend/services/imap_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ async def _run_loop(self):
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error in ImapSyncWorker loop: {e}", exc_info=True)
logger.error("Error in ImapSyncWorker loop", exc_info=True)

# Sleep for 1 minute before the next sync
if self._is_running:
Expand Down
16 changes: 8 additions & 8 deletions backend/services/llm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ async def extract_action_items_and_summary(
),
)
except Exception as e:
logger.error(f"Error calling LLM API for extraction: {e}")
raise LLMServiceError(f"LLM API error during extraction: {e}") from e
logger.error("Error calling LLM API for extraction", exc_info=True)
raise LLMServiceError("LLM API error during extraction") from e
finally:
await client.close()

Expand Down Expand Up @@ -147,8 +147,8 @@ async def translate_email_body(
),
)
except Exception as e:
logger.error(f"Error calling LLM API for translation: {e}")
raise LLMServiceError(f"LLM API error during translation: {e}") from e
logger.error("Error calling LLM API for translation", exc_info=True)
raise LLMServiceError("LLM API error during translation") from e
finally:
await client.close()

Expand Down Expand Up @@ -189,8 +189,8 @@ async def draft_reply(
messages,
)
except Exception as e:
logger.error(f"Error calling LLM API for drafting: {e}")
raise LLMServiceError(f"LLM API error during drafting: {e}") from e
logger.error("Error calling LLM API for drafting", exc_info=True)
raise LLMServiceError("LLM API error during drafting") from e
finally:
await http_client.aclose()

Expand All @@ -211,8 +211,8 @@ async def draft_reply(
),
)
except Exception as e:
logger.error(f"Error calling LLM API for drafting: {e}")
raise LLMServiceError(f"LLM API error during drafting: {e}") from e
logger.error("Error calling LLM API for drafting", exc_info=True)
raise LLMServiceError("LLM API error during drafting") from e
finally:
await client.close()

Expand Down
2 changes: 1 addition & 1 deletion backend/services/pop3_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def _run_loop(self):
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error in Pop3SyncWorker loop: {e}", exc_info=True)
logger.error("Error in Pop3SyncWorker loop", exc_info=True)

if self._is_running:
try:
Expand Down
6 changes: 3 additions & 3 deletions backend/tests/test_email_client_pop3_imap.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ async def mock_wait_for(coro, timeout):
monkeypatch.setattr(client, "_get_tls_context", lambda: None)
monkeypatch.setattr(email_client.asyncio.get_running_loop(), "create_connection", lambda *args, **kwargs: mock_wait_for(None, None))

with pytest.raises(email_client.SMTPConnectError, match="Error connecting to smtp.example.com: network down"):
with pytest.raises(email_client.SMTPConnectError, match="Error connecting to smtp.example.com"):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
await client._create_connection(timeout=10.0)

@pytest.mark.asyncio
Expand Down Expand Up @@ -321,7 +321,7 @@ async def read_response(self, timeout):
return InnerMockProtocol()
monkeypatch.setattr(email_client, "SMTPProtocol", MockProtocol)

with pytest.raises(email_client.SMTPConnectError, match="Error connecting to smtp.example.com: disconnected"):
with pytest.raises(email_client.SMTPConnectError, match="Error connecting to smtp.example.com"):
await client._create_connection(timeout=10.0)

@pytest.mark.asyncio
Expand Down Expand Up @@ -462,7 +462,7 @@ def mock_resolve(*args, **kwargs):
smtp_server="smtp.example.com",
smtp_port=587,
)
with pytest.raises(Exception, match="Failed to send email: Something went wrong"):
with pytest.raises(Exception, match="Failed to send email"):
await email_client.send_email(params, smtp_config)

@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_email_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ def test_parse_eml_mocked_oserror():
with patch("builtins.open", side_effect=OSError("Mocked OS Error")):
with pytest.raises(
EmailParseError,
match=r"Failed to read file dummy\.eml: Mocked OS Error",
match=r"Failed to read file dummy\.eml",
Comment thread
seonghobae marked this conversation as resolved.
Outdated
):
parse_eml("dummy.eml")

Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async def test_generate_embeddings_api_error():
mock_settings.OPENAI_EMBEDDING_MODEL = "test-model"
mock_settings.OPENAI_BASE_URL = None

with pytest.raises(EmbeddingGenerationError, match="Failed to generate embeddings: API error"):
with pytest.raises(EmbeddingGenerationError, match="Failed to generate embeddings"):

await generate_embeddings(["test"], "test-key")
mock_client.close.assert_awaited_once()
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ async def test_webhook_handler_http_error():
data = response.json()
assert data["status"] == "failed"
assert (
"Webhook execution failed: Simulated HTTP Error" in data["message"]
"Webhook execution failed" in data["message"]
)

finally:
Expand Down
Loading