Back to skill

Security audit

avanza-investment-tracker

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a legitimate local investment tracker, but it needs Review because its declared file access is narrower than what the code actually does and it can send portfolio holdings to external APIs by default.

Review before installing. Use it only with a private data directory, understand that portfolio asset names may be sent to Avanza unless you pass --update-prices never, and be careful with import/export paths and destructive commands using --yes. Backup files can contain the same sensitive financial data as the main database.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:8
Finding

Filesystem Access Is Broader Than the Declared Skill Permissions

Content
View full analysis
.pre-..bak so each destructive run gets a distinct backup. Returns the backup path, or None if the source file does not exist (new/empty database). """ src = db.db_file if not src or not os.path.exists(src): return None stamp = datetime.now().strftime("%Y%m%d-%H%M%S") dst = f"{src}.pre-{label}.{stamp}.bak" shutil.copy2(src, dst) logging.info(f"Automatic backup written before destructive operation: {dst}") return dst ``` It accepts and reads arbitrary CSV and special-case JSON paths: ```python special_cases = SpecialCases(args.special_cases) if args.special_cases else None rows_added = data_parser.add_data( args.file, allow_unsettled=getattr(args, 'allow_unsettled', False) ) ``` ```python try: with open(file_path, "r", encoding="utf-8") as special_cases_file: special_cases = json.load(special_cases_file) except UnicodeDecodeError: with open(file_path, "r", encoding="cp1252") as special_cases_file: special_cases = json.load(special_cases_file) ``` ```python try: with open(file_path, "r", encoding="utf-8") as avanza_data_file: avanza_data = csv.reader(avanza_data_file, delimiter=';') avanza_header_row = next(avanza_data) new_format ...[truncated 3268 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/risk_calculator.py:623
Finding

Database Snapshots Use Non-Atomic Temporary File Creation

Content
View full analysis
dict: """Run transaction parser chronologically in a temporary DB and capture holdings.""" db_file = self.db.db_file temp_db_path = f"{db_file}_temp_risk_{int(time.time())}_{id(self)}.db" shutil.copy2(db_file, temp_db_path) temp_db = DatabaseHandler(temp_db_path) temp_db.interpolate = self.interpolate temp_db.connect() ``` Cleanup occurs only later and suppresses deletion failures: ```python finally: temp_db.disconnect() try: os.remove(temp_db_path) except Exception: pass ``` ### Technical Analysis The destination path is generated from the database path, the current timestamp, and a Python object identity. The code does not atomically create the destination with exclusive semantics before calling `shutil.copy2`. If another local process can write to the database directory and can predict, infer, or race the generated pa ...[truncated 2265 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (43)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 65)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 66)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 67)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 68)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 69)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 70)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 71)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 72)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 73)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 74)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 114)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 117)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 120)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 121)May include surrounding context.

md
| `python scripts/cli.py import FILE [--allocate-virtual] [--allow-unsettled]` | Import transaction entries from Avanza CSV (auto-allocate buys to virtuals; def

Hidden Instructions

High
Category
Prompt Injection
Confidence
60% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · test/data/new_format_data.csv (reported line 1)May include surrounding context.

text
Datum;Konto;Typ av transaktion;Värdepapper/beskrivning;Antal;Kurs;Belopp;Transaktionsvaluta;Courtage;Valutakurs;Instrumentvaluta;ISIN;Resultat
2024-04-11;SavingsAccount;Insättning;Direktinsättning från Nordea Bank;;;30000;SEK;;;;;
2024-03-29;SavingsAccount;Preliminärskatt kapitalränta;;;;-18,09;SEK;;;;;
2024-03-29;SavingsAccount;Inlåningsränta;;;;60,3;SEK;;;;;

Anti-Refusal Statement

High
Category
Anti-Refusal
Confidence
80% confidence
Finding

Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.

Content

Scanner excerpt · test/test_price_interpolation.py (reported line 118)May include surrounding context.

python
def test_cli_default_interpolation_no_warning(cli_test_db, capsys):
    # Valuation date '2026-01-07' which falls between 2026-01-02 and 2026-01-12.
    # By default, it will interpolate. No warnings should print.
    args = argparse.Namespace(
        database=str(cli_test_db),
        account='1111',

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Scope Creep

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The manifest says filesystem access is limited to a user-specified local SQLite database and no other files, but the documentation clearly describes reading transaction CSV files, reading/writing special_cases.json, and creating .bak backup files. This mismatch weakens user consent and enforcement boundaries: a reviewer or runtime relying on the manifest could underestimate the skill's real file access and allow broader local file handling than disclosed.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The workflow documentation exposes an irreversible destructive command (reset --hard) with only a brief inline comment and no explicit warning, confirmation guidance, backup step, or recovery limitation. In a skill that manages local portfolio and transaction data, this increases the chance of accidental data loss by users or downstream agents that may follow documented commands literally.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The code drops existing tables with DROP TABLE IF EXISTS as part of _ensure_per_account_tables, which is a destructive database operation. Although there is logging after the fact, there is no confirmation prompt or explicit warning before the deletion, and the class/docstrings do not clearly disclose that cached tables will be removed and recreated.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The _drop_old_tables method performs DROP TABLE IF EXISTS on stored statistics tables, permanently removing cached data. The method lacks a pre-action warning, confirmation step, or prominent documentation explaining that invoking it deletes database tables.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
89% confidence
Finding

This code transmits portfolio-derived metadata to third-party services by querying external endpoints with currency pair lookups. While the payload here is limited, the skill context explicitly handles sensitive financial data, so outbound network activity increases privacy and data exposure risk, especially if users do not expect local portfolio tooling to contact remote services.

Content

Scanner excerpt · scripts/calculate_stats.py (reported line 1362)May include surrounding context.

python
if currency in fx_rates:
                return fx_rates[currency]
            try:
                r = requests.post(url, headers=headers, timeout=10, json={
                    "query": f"{currency}/SEK",
                    "searchFilter": {"types": ["INDEX"]},
                    "pagination": {"from": 0, "size": 1},

External Transmission

Medium
Category
Data Exfiltration
Confidence
96% confidence
Finding

The function sends each asset name from the local portfolio database to Avanza's search API, which can directly reveal a user's holdings, interests, or strategy to an external service. In a financial tracking skill, holdings data is highly sensitive, so this outbound transmission is materially more dangerous than generic telemetry and creates a meaningful confidentiality/privacy risk.

Content

Scanner excerpt · scripts/calculate_stats.py (reported line 1386)May include surrounding context.

python
return None

        for (asset,asset_id) in assets:
            r = requests.post(url, headers=headers, json={"query": asset, "limit": 5}, timeout=10)
            time.sleep(0.05)

            if r.status_code == 200:

Tainted flow: 'fund_url' from requests.post (line 1410, network input) → requests.get (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/calculate_stats.py (reported line 1411)May include surrounding context.

python
try:
                                    if asset_type == "FUND":
                                        fund_url = f"https://www.avanza.se/_api/fund-reference/reference/{order_book_id}"
                                        r_detail = requests.get(fund_url, headers=headers, timeout=10)
                                        time.sleep(0.05)
                                        if r_detail.status_code == 200:
                                            detail_data = r_detail.json()

Tainted flow: 'detail_url' from requests.post (line 1422, network input) → requests.get (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/calculate_stats.py (reported line 1423)May include surrounding context.

python
detail_success = True
                                    elif asset_type in ("STOCK", "CERTIFICATE"):
                                        detail_url = f"https://www.avanza.se/_api/market-guide/{asset_type.lower()}/{order_book_id}"
                                        r_detail = requests.get(detail_url, headers=headers, timeout=10)
                                        time.sleep(0.05)
                                        if r_detail.status_code == 200:
                                            detail_data = r_detail.json()

Static analysis

No suspicious patterns detected.