Skip to content

Commit 5242ef0

Browse files
fix: address Skill Scanner findings - input sanitization for injection prevention
- Add _sanitize_object_name() to validate script interface inputs (block newlines, quotes, semicolons) - Add _sanitize_adql_string() to escape quotes and block injection chars in ADQL queries - Use sanitizers in query_object, query_identifiers, get_all_identifiers - Add best practice #11 in SKILL.md: validate user input before querying Made-with: Cursor
1 parent 9623c9b commit 5242ef0

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

scientific-skills/simbad-database/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ for row in response.json().get("data", []):
372372
8. **Verify object types**: Use the condensed object type (`otype`) for filtering, not the long description
373373
9. **Cache results locally**: Store frequently accessed object data to minimize API calls
374374
10. **Use VOTable format for large TAP results**: It preserves data types and units better than JSON
375+
11. **Validate user input before querying**: When building ADQL or script queries from user-supplied object names, sanitize input to prevent injection. Use the provided `simbad_client.py` which validates and escapes inputs safely.
375376

376377
## Resources
377378

scientific-skills/simbad-database/scripts/simbad_client.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,35 @@
3232
SCRIPT_URL = "https://simbad.cds.unistra.fr/simbad/sim-script"
3333
TAP_URL = "https://simbad.cds.unistra.fr/simbad/sim-tap/sync"
3434

35+
# Characters that could enable script/SQL injection - must not appear in user input
36+
_FORBIDDEN_IN_NAME = frozenset("\n\r\t'\"\\;<>")
37+
38+
39+
def _sanitize_object_name(name: str) -> str:
40+
"""Validate and sanitize object name for script interface (no injection)."""
41+
if not name or not isinstance(name, str):
42+
raise ValueError("Object name must be a non-empty string")
43+
name = " ".join(name.split()) # collapse whitespace
44+
if len(name) > 128:
45+
raise ValueError("Object name too long")
46+
if any(c in _FORBIDDEN_IN_NAME for c in name):
47+
raise ValueError("Object name contains disallowed characters")
48+
return name.strip()
49+
50+
51+
def _sanitize_adql_string(s: str) -> str:
52+
"""Escape single quotes for safe use in ADQL string literals."""
53+
if not s or not isinstance(s, str):
54+
raise ValueError("Identifier must be a non-empty string")
55+
if len(s) > 128:
56+
raise ValueError("Identifier too long")
57+
# Block newlines, semicolons, backslashes (injection vectors)
58+
bad = frozenset("\n\r\t\\;<>\"")
59+
if any(c in bad for c in s):
60+
raise ValueError("Identifier contains disallowed characters")
61+
return s.replace("'", "''") # ADQL string literal escape
62+
63+
3564
FORMAT_STRINGS = {
3665
"basic": "%IDLIST(1) | %COO(A D;ICRS) | %OTYPE",
3766
"detailed": "%IDLIST(1) | %COO(A D;ICRS) | %OTYPE | %SP | %FLUXLIST(V)",
@@ -98,11 +127,12 @@ def query_object(
98127
Returns:
99128
List of result dicts with keys like main_id, coordinates, object_type, etc.
100129
"""
130+
safe_name = _sanitize_object_name(name)
101131
fmt = FORMAT_STRINGS.get(output_format, FORMAT_STRINGS["basic"])
102132
script = "\n".join([
103133
"output console=off script=off",
104134
f'format object "{fmt}"',
105-
f"query id {name}",
135+
f"query id {safe_name}",
106136
])
107137
text = _execute_script(script)
108138
return _parse_script_response(text)
@@ -154,11 +184,12 @@ def query_identifiers(
154184
Returns:
155185
List of result dicts
156186
"""
187+
safe_pattern = _sanitize_object_name(pattern)
157188
fmt = FORMAT_STRINGS.get(output_format, FORMAT_STRINGS["basic"])
158189
script = "\n".join([
159190
"output console=off script=off",
160191
f'format object "{fmt}"',
161-
f"query id wildcard {pattern}",
192+
f"query id wildcard {safe_pattern}",
162193
])
163194
text = _execute_script(script)
164195
return _parse_script_response(text, max_results=max_results)
@@ -208,10 +239,11 @@ def get_all_identifiers(name: str) -> List[str]:
208239
Returns:
209240
List of identifier strings
210241
"""
242+
safe_name = _sanitize_adql_string(name)
211243
result = tap_query(
212244
f"SELECT i.id FROM ident AS i "
213245
f"JOIN basic AS b ON i.oidref = b.oid "
214-
f"WHERE b.main_id = '{name}'",
246+
f"WHERE b.main_id = '{safe_name}'",
215247
max_results=500,
216248
fmt="json",
217249
)

0 commit comments

Comments
 (0)