Back to skill

Security audit

TCS Expense Claim Processor

Security checks for vulnerabilities and agentic risk

Overview

This expense-claim skill is mostly purpose-aligned, but its helper scripts allow overbroad local file reads/writes and it can generate sensitive claim files even for narrower summary requests.

Review before installing. Only use this skill with receipts you intend to package for reimbursement, and avoid feeding it externally supplied manifests or untrusted expense text until path containment, filename sanitization, spreadsheet text escaping, and safer dependency installation guidance are added.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
build_pdfs.py:50
Finding

Unrestricted Local File Read Through Manifest-Controlled Paths

Content
View full analysis

Vulnerability Details

File Location: build_pdfs.py, lines 50–64
Vulnerability Type: Improper path authorization and unrestricted file access
Risk Level: High

Vulnerable Code

python
def make_pdf(out_path: str, files: list[str]) -> int:
    """Merge images and PDFs into a single output PDF. Returns page count."""
    writer = PdfWriter()
    for fpath in files:
        if not os.path.exists(fpath):
            print(f"  [WARN] Missing file, skipping: {fpath}")
            continue
        ext = fpath.lower().rsplit(".", 1)[-1]
        if ext in ("jpg", "jpeg", "png", "webp", "bmp", "tiff", "gif"):
            pdf_bytes = img_to_pdf_bytes(fpath)
            reader = PdfReader(io.BytesIO(pdf_bytes))
        elif ext == "pdf":
            reader = PdfReader(fpath)
        else:
            print(f"  [WARN] Unknown file type, skipping: {fpath}")
            continue
        for page in reader.pages:
            writer.add_page(page)

Technical Analysis

File paths supplied through the JSON manifest are passed directly to Image.open() or PdfReader() after only an existence check. The implementation does not canonicalize paths, enforce containment within /mnt/user-data/uploads/, reject symbolic links, or verify that the path refers to an authorized regular file.

Consequently, an attacker who can control the manifest can request any readable local image or PDF. The contents are incorporated into a generated output PDF, creating a channel through which files outside the legitimate receipt-upload directory can be accessed and disclosed.

Attack Path

  1. The attacker prepares a manifest containing an absolute path or traversal path to a sensitive local image or PDF.
  2. The attacker invokes build_pdfs.py with that manifest.
  3. make_pdf() confirms only that the target exists.
  4. Image.open() or PdfReader() reads the unauthorized local file.
  5. The pages are c ...[truncated 498 chars]
Remediation
View remediation

Remediation Suggestions

  • Resolve every supplied path with os.path.realpath() or pathlib.Path.resolve().
  • Verify that each resolved path is contained beneath the canonical /mnt/user-data/uploads/ directory.
  • Reject absolute paths, traversal components, symbolic links, device files, and non-regular files.
  • Use an allowlist of server-generated upload identifiers rather than accepting arbitrary filesystem paths.
  • Open files using race-resistant mechanisms where supported, including O_NOFOLLOW.
  • Apply file-size, extension, and content-signature validation before parsing.

Example containment policy:

python
from pathlib import Path

UPLOAD_ROOT = Path("/mnt/user-data/uploads").resolve()

def authorized_upload(value: str) -> Path:
    candidate = Path(value).resolve(strict=True)
    if UPLOAD_ROOT not in candidate.parents:
        raise ValueError("File is outside the authorized upload directory")
    if not candidate.is_file() or candidate.is_symlink():
        raise ValueError("Only regular, non-symlink upload files are allowed")
    return candidate

T09 · Insecure Skill Coding Practices

Error
Location
build_pdfs.py:92
Finding

Arbitrary PDF Write Through Output Path Traversal

Content
View full analysis

Vulnerability Details

File Location: build_pdfs.py, lines 70–72 and 92–98
Vulnerability Type: Path traversal and arbitrary file write
Risk Level: High

Vulnerable Code

python
    os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
    with open(out_path, "wb") as f:
        writer.write(f)
    return len(writer.pages)
python
    for entry in manifest:
        date = entry["date"]          # e.g. "13_Feb_2026"
        category = entry["category"]  # e.g. "Travel_Expenses_Conveyance"
        files = entry["files"]
        fname = f"{date}_{category}.pdf"
        out_path = os.path.join(args.out, fname)
        pages = make_pdf(out_path, files)

Technical Analysis

The date and category fields are controlled by the manifest and are concatenated into a filename without schema validation or path sanitization. The resulting value is joined to the output directory and passed to open(..., "wb").

Traversal sequences can escape the configured output directory. A leading path separator can also cause os.path.join() to discard the intended output directory when the constructed filename is absolute. The destination is then created or overwritten with PDF data under the privileges of the script.

Although the constructed destination ends in .pdf, this still permits modification of arbitrary writable PDF-named paths and creation of files outside the expense output directory.

Attack Path

  1. The attacker supplies a manifest whose date or category contains path separators, traversal segments, or an absolute-path prefix.
  2. The script constructs fname directly from those values.
  3. os.path.join() produces a path outside args.out.
  4. os.makedirs() creates attacker-selected parent directories where permissions allow.
  5. open(out_path, "wb") creates or truncates the selected destination.
  6. PdfWriter.write() places attacker-influenced PDF cont ...[truncated 410 chars]
Remediation
View remediation

Remediation Suggestions

  • Validate date against a strict format such as ^\d{2}_[A-Za-z]{3}_\d{4}$.
  • Map categories from a fixed allowlist rather than treating them as filename fragments.
  • Reject path separators, absolute paths, . components, and .. components.
  • Resolve the final destination and verify that it remains beneath the canonical output directory before creating it.
  • Generate output filenames internally rather than accepting filesystem syntax from the manifest.
  • Refuse to overwrite existing files unless explicitly authorized.

Example destination check:

python
from pathlib import Path

output_root = Path(args.out).resolve()
output_root.mkdir(parents=True, exist_ok=True)

destination = (output_root / fname).resolve()
if output_root not in destination.parents:
    raise ValueError("Output path escapes the configured directory")

T09 · Insecure Skill Coding Practices

Warning
Location
build_xlsx.py:130
Finding

Spreadsheet Formula Injection in Generated Expense Workbooks

Content
View full analysis

Vulnerability Details

File Location: build_xlsx.py, lines 130–143
Vulnerability Type: Spreadsheet formula injection
Risk Level: Medium

Vulnerable Code

python
    for i, exp in enumerate(expenses):
        row = 5 + i
        fk = CATEGORY_FILL.get(exp.get("category", ""), "other")
        vals = [
            i + 1,
            exp.get("date", ""),
            exp.get("vendor", ""),
            exp.get("location", ""),
            exp.get("category", ""),
            exp.get("currency", ""),
            exp.get("amount_orig", 0),
            exp.get("amount_inr", 0),
            exp.get("notes", ""),
        ]
        for col, val in enumerate(vals, 1):
            cell = ws1.cell(row=row, column=col)
            cell.value = val

Technical Analysis

Text originating from expense data is assigned directly to openpyxl cells. Spreadsheet applications may interpret strings beginning with =, +, -, or @ as formulas rather than literal text.

If receipt-derived or attacker-controlled fields such as vendor, location, category, or notes contain a formula payload, the generated workbook can preserve it as executable spreadsheet content. When a reviewer opens the file, the spreadsheet client may evaluate the formula. Depending on the client and its security configuration, formulas can initiate external requests, disclose cell data, misrepresent claim values, or invoke legacy dangerous functionality.

Attack Path

  1. The attacker places a formula-prefixed value in an expense text field, potentially through receipt text that is copied into the structured JSON.
  2. build_xlsx.py assigns the value directly to cell.value.
  3. The generated XLSX stores the value as formula-capable spreadsheet content.
  4. A finance employee or reviewer opens the workbook.
  5. The spreadsheet application evaluates or prompts to evaluate the formula.
  6. The payload may initiat ...[truncated 507 chars]
Remediation
View remediation

Remediation Suggestions

  • Treat every receipt-derived textual field as untrusted.
  • Before assigning text to a cell, prefix values beginning with =, +, -, or @ with an apostrophe.
  • Also account for leading tabs, carriage returns, spaces, and other characters that some spreadsheet clients ignore before formula detection.
  • Apply sanitization consistently to all sheets, including category summaries and configuration-derived headings.
  • Validate numeric fields as actual finite numeric types rather than accepting strings.
  • Add automated tests covering common CSV/XLSX injection payloads.

Example defensive conversion:

python
def safe_spreadsheet_text(value):
    text = str(value)
    normalized = text.lstrip(" \t\r\n")
    if normalized.startswith(("=", "+", "-", "@")):
        return "'" + text
    return text

Use this function for every untrusted text value before assigning it to cell.value.

T08 · Insecure Dependencies

Warning
Location
build_pdfs.py:27
Finding

Unpinned System-Level Dependency Installation Guidance

Content
View full analysis

Vulnerability Details

File Location: build_pdfs.py, lines 27–38; build_xlsx.py, lines 39–44
Vulnerability Type: Unsafe and non-reproducible dependency installation
Risk Level: Medium

Vulnerable Code

python
try:
    import img2pdf
    from PIL import Image
    from pypdf import PdfReader, PdfWriter
except ImportError as e:
    print(f"Missing dependency: {e}")
    print("Run: pip install img2pdf Pillow pypdf --break-system-packages")
    sys.exit(1)
python
try:
    from openpyxl import Workbook
    from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
    from openpyxl.utils import get_column_letter
except ImportError:
    print("Run: pip install openpyxl --break-system-packages")
    sys.exit(1)

Technical Analysis

The scripts instruct operators to install dependencies without version constraints or package hashes. Package resolution therefore depends on the active package index and whichever releases are current at installation time.

The use of --break-system-packages bypasses protections intended to prevent pip from modifying an externally managed Python environment. This can destabilize system tooling and broadens the effect of a compromised or incompatible package installation.

No malicious dependency is embedded in the audited project, and the listed package names are not themselves evidence of malicious behavior. The risk arises from unsafe installation guidance, mutable dependency resolution, and reliance on the operator's active pip index configuration.

Attack Path

  1. A required module is absent, causing an import failure.
  2. The script prints the suggested pip command.
  3. An operator runs the command in an environment using a compromised, malicious, or attacker-configured package index.
  4. Pip resolves unpinned package versions from that source.
  5. Package installation executes build or installation logic and modifies ...[truncated 649 chars]
Remediation
View remediation

Remediation Suggestions

  • Remove --break-system-packages from all installation guidance.
  • Provide a version-controlled dependency lock file with exact versions and cryptographic hashes.
  • Install dependencies in a dedicated virtual environment or isolated container.
  • Use pip install --require-hashes -r requirements.txt.
  • Configure an explicitly trusted package index and disable unexpected extra indexes.
  • Scan and update dependencies through a controlled review process.
  • Document supported Python and dependency versions so builds remain reproducible.
  • Consider distributing a prebuilt, signed environment or container image for production use.
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (3)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The declared description promises an end-to-end expense claim processor that can organize receipts, categorize expenses, prepare claims, summarize spending, and perform currency conversion. The supplied code does only one narrow subtask: building merged PDF bundles from a manifest that already contains date, category, and file paths. It does not analyze receipts, derive categories, compute totals, convert currencies, or submit/export claims to external systems. While PDF generation is mentioned in the description, the actual code is materially narrower than the declared primary purpose, so this is a mismatch.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The trigger language is broad enough that the skill may be invoked for ordinary expense conversations or single-file uploads without clear user intent to generate claim artifacts. In this context, unintended invocation can cause unnecessary processing of sensitive financial documents and lead to unwanted file generation or retention in output directories.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill directs writing PDFs and an XLSX into persistent user-data output paths without clearly warning the user before artifacts are created. Because receipts and invoices contain sensitive personal and financial data, silent file creation increases privacy, retention, and accidental disclosure risk, especially if users expected only an in-chat summary.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.