T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/build_xlsx.py:83
- Finding
- Untrusted Tavily Results Can Cause Spreadsheet Formula Injection## Vulnerability Details **File Location**: `scripts/build_xlsx.py:83-91, 333-347` **Vulnerability Type**: Spreadsheet formula injection **Risk Level**: Medium ### Complete Code Snippet ```python def fill_row(ws, row: int, values: list, fill: PatternFill, height: int = None) -> None: for j, v in enumerate(values, start=1): cell = ws.cell(row=row, column=j, value=v) cell.alignment = WRAP cell.border = BORDER cell.fill = fill if height: ws.row_dimensions[row].height = height ``` The vulnerable helper is used to write Tavily source metadata directly into workbook cells: ```python fuentes = data.get("fuentes_tavily") or [] if fuentes: ws7 = wb.create_sheet("Fuentes Tavily") style_title(ws7, 1, 2, "FUENTES CONSULTADAS (TAVILY)") for j, h in enumerate(["Título", "URL"]): ws7.cell(row=2, column=j + 1, value=h) style_header(ws7, 2, 2) for i, f in enumerate(fuentes): fill = ALT_FILL if i % 2 == 0 else WHITE_FILL fill_row(ws7, 3 + i, [f.get("titulo", ""), f.get("url", "")], fill, 28) autosize(ws7, [50, 80]) ws7.freeze_panes = "A3" out_path.parent.mkdir(parents=True, exist_ok=True) wb.save(out_path) ``` ### Technical Analysis `SKILL.md:215-248` directs the agent to perform Tavily web searches and retain consulted source titles and URLs. Those values originate from independently controlled web pages and therefore cross an external-content trust boundary. `build_xlsx.py` reads these values from `fuentes_tavily` and passes them unchanged to `openpyxl` through `Worksheet.cell(..., value=v)`. Values beginning with spreadsheet formula prefixes—most importantly `=`—are not neutralized or forced to the string data type. Consequently, a malicious page title such as a formula expression can be stored as an active formula rather than displayed as literal text. The generic `fill_row` function also writes othe ...[truncated 1955 chars]
- Remediation
- ## Remediation Suggestions Treat every value obtained from Tavily or other external content as untrusted before writing it to a workbook. 1. Introduce a centralized literal-cell sanitizer and apply it in `fill_row`: ```python FORMULA_PREFIXES = ("=", "+", "-", "@") def spreadsheet_literal(value): if isinstance(value, str) and value.startswith(FORMULA_PREFIXES): return "'" + value return value def fill_row(ws, row: int, values: list, fill: PatternFill, height: int = None) -> None: for j, value in enumerate(values, start=1): cell = ws.cell( row=row, column=j, value=spreadsheet_literal(value), ) cell.alignment = WRAP cell.border = BORDER cell.fill = fill if height: ws.row_dimensions[row].height = height ``` 2. Apply the same protection to every direct cell assignment containing brief, generated, or Tavily-derived data—not only to the source-title column. 3. Alternatively, explicitly force untrusted cells to the string data type after assignment, while verifying through tests that `openpyxl` serializes them as literal strings. 4. Add regression tests covering values beginning with `=`, `+`, `-`, and `@`, including formulas that reference external resources. 5. Keep legitimate hyperlinks separate from displayed text. Validate URL schemes against a narrow allowlist such as `https` before assigning hyperlink targets.
