Read-Only Access, Field Whitelisting, and False Positive Control in Personal Finance Assistants

    This article explores the security architecture of a personal finance assistant powered by natural language processing. It covers threat models, system boundaries, strict read-only authorization with scope validation, field whitelisting, data masking, and transaction standardization. These measures ensure secure data handling while mitigating risks associated with credential leakage, prompt injection, and excessive false positives in anomaly detection.

    1. Threat Model and Boundaries

    A personal finance assistant aims to solve two main problems: scattered bills across multiple bank and credit cards making unified viewing difficult, and the high cost of manual bookkeeping, which often leads to abandonment after a few months. Natural language queries can address both issues. For example, a user can ask, “How much more did I spend on dining this month compared to last month?” and the system aggregates and compares the data to provide a direct answer.

    Before designing the system, the primary threats must be identified:

    • Credential or access token leakage, allowing attackers to access account data.
    • Prompt injection via user text or transaction descriptions, tricking the model into calling out-of-scope tools.
    • Logs, caches, or analysis results retaining excessive personal information.
    • Excessive false positives in anomaly detection eroding user trust, or false negatives causing risks to go unreported.

    This article establishes two boundaries. First, the model only interacts with processed results and never directly reads raw account data. Second, the system has no fund operation capabilities. Both boundaries are strictly enforced on the server side and do not rely on prompt constraints. While these measures reduce risk, they cannot eliminate it entirely; remaining risks should be documented during the review process.

    2. Read-Only Authorization and Strict Scope Validation

    Permissions determine the risk ceiling of the system and must be defined before feature development.

    2.1 Read-Only Tokens

    Analysis applications should only request query permissions, not transfers, order placements, or account setting modifications. During upstream authorization, the token’s scope should only include accounts:read and transactions:read.

    Read-only tokens minimize potential losses but do not guarantee security on their own. The server must validate the scope on every call. If validation fails, the request is rejected immediately and an audit log is recorded. Example code is as follows:

    1. REQUIRED_SCOPES = {"accounts:read", "transactions:read"}
    2. WRITE_SCOPES = {"transfer:write", "payment:write", "account:write"}
    3. def check_scope(token_scopes: set[str]) -> None:
    4. if not REQUIRED_SCOPES.issubset(token_scopes):
    5. raise PermissionError("Token scope insufficient")
    6. if token_scopes & WRITE_SCOPES:
    7. raise PermissionError("Token contains write permissions, rejected")

    2.2 Revoking Authorization

    Upon user authorization revocation, the system must complete three tasks:

    1. Call the upstream revocation API to invalidate the existing access token immediately.
    2. Delete locally cached account and transaction data. The deletion task must be completed within 24 hours of revocation and recorded. The record should include the task ID, data category, completion time, and execution result, but not the data content itself.
    3. During the next re-authorization, the interface must list the read scope again, and data fetching only begins after user confirmation.

    The 24-hour period is an example. The actual timeframe should comply with local regulations and user agreements, and be stored in a configuration center rather than hardcoded in business logic.

    2.3 Lookback Period

    The system fetches the last 24 months of transaction records by default. Users can shorten this period or extend it up to the product limit. Data beyond the period is not fetched, and cached portions are deleted once the retention policy expires. The 24-month window is chosen to cover two complete years of seasonal spending, facilitating year-over-year analysis.

    3. Field Whitelisting and Data Masking

    3.1 Whitelist

    Allowed fields at the account level include institution name, account name, account type, last four digits of the account number, balance, and credit limit. Allowed fields at the transaction level include transaction time, amount, currency, merchant name, transaction description, and status. Full account numbers, ID numbers, phone numbers, and login credentials are excluded from the analysis pipeline.

    1. ALLOWED_FIELDS = {"institution", "account_name", "account_type",
    2. "last4", "balance", "credit_limit"}
    3. ALLOWED_TX_FIELDS = {"posted_at", "amount", "currency",
    4. "merchant", "description", "status"}
    5. def filter_fields(rows: list[dict], allowed: set[str]) -> list[dict]:
    6. return [{k: v for k, v in row.items() if k in allowed} for row in rows]
    7. def get_accounts(user_id: str, token_scopes: set[str]) -> list[dict]:
    8. check_scope(token_scopes)
    9. raw = account_gateway.fetch_accounts(user_id) # Upstream read-only call
    10. return filter_fields(raw, ALLOWED_FIELDS)

    3.2 Data Masking

    Fields within the whitelist may still contain personal information. Merchant names and transaction descriptions sometimes include names, room numbers, phone numbers, or transfer remarks. Processing rules are as follows:

    • Before entering the analysis pipeline, use regular expressions to replace phone numbers, ID card formats, and long numeric strings with [MASKED].
    • Do not log the original transaction description; only log the transaction ID and field names.
    • Transaction descriptions sent to the model are truncated to a maximum of 20 characters.
    • Each tool function independently validates parameters and return fields, without assuming upstream filtering.

    4. Standardization and Matching

    Raw transaction descriptions are often unstandardized. The same merchant may have abbreviations, branch names, and acquirer codes. Standardization follows three steps:

    1. Remove acquirer codes, date suffixes, and meaningless characters.
    2. Map different variations of the same merchant to a standard name.
    3. Match refunds, reversals, and original transactions. Matched records are not counted as expenditures.

    When matching fails—meaning no corresponding original transaction is found, or the amount is inconsistent—the record is not included in the expenditure calculation.