T09 · Insecure Skill Coding Practices
- Location
scripts/UpdateStock_skill.py:1436- Finding
SQL Injection in MCP Stock Query Tools
- Content
View full analysis
= '{start_date}' " if end_date: sql += f"AND trade_date <= '{end_date}' " sql += " ORDER BY trade_date;" with duckdb.connect(DB_path) as conn: df = conn.execute(sql).df() ``` The same vulnerable construction is used by both `get_adj_stock` and `get_stock`. ### Technical Analysis The MCP-controlled values `symbol`, `start_date`, and `end_date` are inserted directly into a DuckDB SQL statement using formatted strings. The Pydantic `Field` declarations only provide descriptions and do not enforce the documented six-digit symbol or date formats. An attacker can supply quotes, SQL operators, comments, or other SQL syntax to change the meaning of the query. For example, a crafted `symbol` could terminate the string literal and add an always-true condition, bypassing the intended stock-symbol filter. More damaging statements may be possible if the installed DuckDB driver accepts multiple statements or if database features accessible to the process permit additional file or database operations. Because the MCP tools may be called from agent-generated arguments, malicious content processed by the agent could indirectly provide the injection payload without requiring direct access to the stdio process. ### Attack Path 1. An attacker supplies malicious content to an agent that can invoke this MCP Skill. 2. The content induces the agent to call `get_stock` or `get_adj_stock` with a quote-breaking value in `symbol`, `start_date`, or `end_date`. 3. The MCP handler concatenates the value into the SQL statement without escaping or parameterization. 4. DuckDB interprets the injected characters as S ...[truncated 785 chars]- Remediation
View remediation
= ?") params.append(start_date) if end_date: clauses.append("trade_date <= ?") params.append(end_date) sql = ( "SELECT * FROM stock WHERE " + " AND ".join(clauses) + " ORDER BY trade_date" ) with duckdb.connect(DB_path) as conn: df = conn.execute(sql, params).df() ``` Additionally: 1. Enforce stock symbols with a full-match expression such as `^\d{6}$`. 2. Parse dates with a strict `YYYY-MM-DD` parser before executing the query. 3. Reject unexpected control characters, quotes, comments, and trailing input. 4. Open the database in read-only mode for query-only MCP tools where supported. 5. Add regression tests containing quote-breaking, comment, union, and stacked-statement payloads. 6. Apply the correction independently to both `get_stock` and `get_adj_stock`. ]]>
