Skip to content

Commit 98b8a56

Browse files
vdavezclaude
andcommitted
Sanitize API error details in exceptions and add bandit to CI
Prevents information disclosure by truncating long API error messages in exception strings. Raw response data remains available via response_data for programmatic access. Adds bandit security linter as a dev dependency with a new CI workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 35f8d77 commit 98b8a56

4 files changed

Lines changed: 112 additions & 2 deletions

File tree

.github/workflows/security.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Security
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
bandit:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@v4
18+
with:
19+
version: "latest"
20+
21+
- name: Set up Python
22+
run: uv python install 3.12
23+
24+
- name: Install dependencies
25+
run: uv sync --all-extras
26+
27+
- name: Run bandit security linter
28+
run: uv run bandit -r tango/ -c pyproject.toml

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dev = [
3636
"pytest-recording>=0.13.0",
3737
"python-dotenv>=1.0.0",
3838
"pyyaml>=6.0",
39+
"bandit>=1.7.0",
3940
]
4041
notebooks = [
4142
"jupyter>=1.0.0",
@@ -113,6 +114,10 @@ exclude_lines = [
113114
"if __name__ == .__main__.:",
114115
]
115116

117+
[tool.bandit]
118+
exclude_dirs = ["tests", "scripts"]
119+
skips = []
120+
116121
[tool.hatch.build.targets.wheel]
117122
packages = ["tango"]
118123

tango/client.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ def _int_or_none(val: str | None) -> int | None:
143143
burst_reset=_int_or_none(headers.get("X-RateLimit-Burst-Reset")),
144144
)
145145

146+
@staticmethod
147+
def _sanitize_error_detail(detail: Any, max_length: int = 200) -> str:
148+
"""Sanitize an error detail from an API response for safe inclusion in exception messages.
149+
150+
Truncates long messages to prevent information disclosure in logs
151+
and error tracking systems.
152+
"""
153+
text = str(detail)
154+
if len(text) > max_length:
155+
text = text[:max_length] + "..."
156+
return text
157+
146158
def _request(
147159
self,
148160
method: str,
@@ -176,7 +188,10 @@ def _request(
176188
or error_data.get("error")
177189
)
178190
if detail:
179-
error_msg = f"Invalid request parameters: {detail}"
191+
error_msg = (
192+
f"Invalid request parameters: "
193+
f"{self._sanitize_error_detail(detail)}"
194+
)
180195
raise TangoValidationError(
181196
error_msg,
182197
response.status_code,
@@ -185,7 +200,9 @@ def _request(
185200
elif response.status_code == 429:
186201
error_data = response.json() if response.content else {}
187202
detail = error_data.get("detail", "Rate limit exceeded")
188-
raise TangoRateLimitError(detail, response.status_code, error_data)
203+
raise TangoRateLimitError(
204+
self._sanitize_error_detail(detail), response.status_code, error_data
205+
)
189206
elif not response.is_success:
190207
raise TangoAPIError(
191208
f"API request failed with status {response.status_code}", response.status_code

uv.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)