Skip to content

Commit 94cc9d0

Browse files
Security remediation: credentials hygiene, HTTP safety, output safety, typed errors, test suite
Addresses several classes of defects in the existing codebase. None of these changes alter the public CLI surface beyond removing the --password flag. Credentials & logging - Load the OAuth client secret from SIMPLIFI_CLIENT_SECRET instead of embedding it in source. Missing env var raises a clear RuntimeError. - Remove --password CLI argument. Password is resolved from SIMPLIFI_PASSWORD or, on a TTY, prompted via getpass.getpass(). - MFA code is now read via getpass.getpass() rather than input(), so it does not echo or enter readline history. - The bearer token, refresh token, MFA channel (a masked but still user-identifying string), and Simplifi user id are no longer logged at any level. HTTP hygiene & SSRF - Every requests call passes timeout=30. No more indefinite hangs. - Pagination follows an upstream-supplied nextLink: that value is now validated to be either a relative path or an absolute URL on https://services.quicken.com/ before the bearer token is sent. - The get_datasets query-string used the variable name 'limit' as a dict key instead of the string literal 'limit', so the limit was never actually sent. Fixed. - Every JSON decode is wrapped: malformed bodies raise a typed SimplifiAPIError instead of bubbling a raw JSONDecodeError. Typed exception hierarchy - New simplifiapi.exceptions module defines SimplifiAPIError and an AuthenticationError subclass. - Client methods raise these instead of returning None / False on failure (verify_token now raises on failure and installs the bearer header on success). - The CLI main() catches SimplifiAPIError at the boundary and SystemExits with a one-line message — no Python traceback that could echo URL params or partial headers. Output safety - --filename runs through os.path.basename so '../../tmp/evil' cannot escape CWD. - CSV cells whose first character is = + - or @ are prefixed with a single quote (formula-injection escape). - JSON output is opened 'w' with utf-8 encoding (was 'w+', no encoding, which mangled non-ASCII on Windows cp1252 locales). Quality cleanups - Collapse four near-identical resource getters (get_accounts/transactions/tags/categories) into _get_resource. - Library is logging-pure: __init__ attaches only a NullHandler; real log config lives in cli.main's _configure_logging() and honours LOG_LEVEL. - Migrate project metadata to pyproject.toml [project] (PEP 621), drop the broken 'name = setuptools' setup.cfg metadata block. - Pin runtime deps: requests>=2.31,<3, pandas>=2,<3, configargparse>=1.7,<2. - Add .env.example template for SIMPLIFI_CLIENT_SECRET and SIMPLIFI_PASSWORD. Tests - New pytest + responses suite covering the cases above (auth happy path, MFA, pagination key, SSRF rejection, timeout presence, CSV formula escape, --filename basename strip, JSON-decode safety, typed exception boundary). 33 tests; coverage configured to fail under 80% (currently 91%). README is rewritten to document the trust model, the new CLI surface, the typed-exception API, and a development setup.
1 parent 27fcc74 commit 94cc9d0

14 files changed

Lines changed: 1544 additions & 178 deletions

.env.example

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# .env.example — copy to .env and fill in real values.
2+
#
3+
# `.env` is excluded by .gitignore; this file (.env.example) is committed
4+
# as a template. NEVER commit a populated .env.
5+
#
6+
# Usage:
7+
# cp .env.example .env
8+
# # edit .env with real values
9+
# set -a && source .env && set +a # POSIX shells
10+
# # or on Windows PowerShell: Get-Content .env | ForEach-Object { ... }
11+
# # then run:
12+
# python -m simplifiapi --email you@example.com --accounts
13+
14+
# ---------------------------------------------------------------------------
15+
# SIMPLIFI_CLIENT_SECRET (required)
16+
# ---------------------------------------------------------------------------
17+
# The OAuth client secret used by the embedded `acme_web` client when calling
18+
# https://services.quicken.com/oauth/token. In upstream rijn/simplifiapi this
19+
# value was hardcoded in the source; this fork removed the literal and reads
20+
# it from this env var at call time (see Phase 1, AUDIT.md finding C1).
21+
#
22+
# Trust model: Quicken cannot revoke this secret per-user. Anyone with the
23+
# value can impersonate the `acme_web` OAuth client. Treat it like a
24+
# moderately-sensitive shared secret. See README.md > Security & Trust Model.
25+
SIMPLIFI_CLIENT_SECRET=replace-me-with-the-real-oauth-client-secret
26+
27+
# ---------------------------------------------------------------------------
28+
# SIMPLIFI_PASSWORD (optional — used only when --token is not supplied)
29+
# ---------------------------------------------------------------------------
30+
# Your Quicken Simplifi account password. Used by `Client.get_token` to
31+
# authenticate. If unset and the tool needs to authenticate, it will fall
32+
# back to `getpass.getpass()` and prompt interactively (only when stdin is
33+
# a TTY). See Phase 1, AUDIT.md finding C2.
34+
#
35+
# NEVER put this on the command line — `--password` was removed in Phase 1
36+
# precisely because argv leaks via `ps`, shell history, and syslog.
37+
SIMPLIFI_PASSWORD=replace-me-with-your-simplifi-password

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,11 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
162+
# simplifiapi CLI output — these contain real account/transaction data.
163+
# The CLI writes <filename>_<resource>.{json,csv} (default filename: 'output').
164+
# Never commit these.
165+
/output_*.json
166+
/output_*.csv
167+
/test_*.json
168+
/test_*.csv

README.md

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,157 @@
11
# simplifiapi
2-
An unofficial API for Quicken Simplifi
2+
3+
An unofficial CLI and Python library for extracting your own Quicken Simplifi data — accounts, transactions, tags, and categories — to JSON or CSV.
4+
5+
## Security & Trust Model
6+
7+
This is an **unofficial** client for Quicken Simplifi. Before running it against your real account, understand the trust model:
8+
9+
- **OAuth client secret comes from your environment.** The OAuth client secret is no longer embedded in source. You must set `SIMPLIFI_CLIENT_SECRET` in your environment before invoking the CLI or the `Client` class. If it is unset, the tool fails loudly rather than silently proceeding.
10+
- **Your Simplifi account password is never accepted on argv.** The `--password` CLI flag has been removed (it leaked via `ps`, shell history, and syslog). Provide your password by either setting `SIMPLIFI_PASSWORD` in your environment, or by letting the CLI prompt you via `getpass.getpass()` when run interactively. Non-interactive runs with no env var exit with a clear error.
11+
- **MFA codes are read via `getpass.getpass()`** — they do not echo to the terminal and do not enter readline history.
12+
- **The bearer token, refresh token, and Simplifi user id are never logged** at any level.
13+
- **HTTP calls are timeout-bounded and host-guarded.** Every request carries a 30-second timeout. The `nextLink` value returned by Quicken during pagination is validated: it must be either a relative path or an absolute URL on `https://services.quicken.com/`. Anything else is rejected before the bearer token is sent.
14+
- **`--filename` cannot escape the working directory.** Any path components in the value are stripped via `os.path.basename` before opening the output file.
15+
- **CSV output is formula-injection-safe.** Cells whose stringified value starts with `=`, `+`, `-`, or `@` are prefixed with a single quote before writing.
16+
- **The OAuth `clientSecret` cannot be rotated per-user.** Because this is an unofficial client, Quicken does not own a per-user relationship for the OAuth `clientSecret` and cannot revoke it for an individual user. Anyone running this client is implicitly accepting that the secret is shared across every user of the project. If the secret leaks, every user is affected — Quicken's only remediation would be invalidating the secret for the entire client, breaking the tool for everyone.
17+
- **Run in an isolated environment.** Treat `SIMPLIFI_CLIENT_SECRET` and `SIMPLIFI_PASSWORD` as sensitive. Prefer a dedicated shell, container, or VM where those env vars do not bleed into your normal development environment or any background process that snapshots `/proc/<pid>/environ`. Export them via a sourced `.env` file you do not commit, not by typing them at a prompt that lands in shell history.
18+
19+
A starter `.env.example` is shipped at the repo root. Copy it to `.env` and fill in your values; do not commit `.env`.
320

421
## Install
522

6-
PyPI is temporarily down. Install with pip from GitHub directly
23+
Install from a clone of this repo:
724

825
```shell
9-
pip3 install git+https://github.com/rijn/simplifiapi
26+
# from a clone of this repo
27+
pip install .
28+
29+
# or, in editable mode for local development
30+
pip install -e .
31+
32+
# or, with dev/test extras (pytest + responses)
33+
pip install -e '.[dev]'
1034
```
1135

36+
Pinned runtime dependencies (from `pyproject.toml`): `requests>=2.31,<3`, `pandas>=2,<3`, `configargparse>=1.7,<2`. Python 3.9+.
37+
1238
## CLI
1339

14-
This package provides a command-line tool that could access and save data to local files.
40+
The package installs a `simplifiapi` entry point that extracts data from your Simplifi account to local files.
41+
42+
```shell
43+
usage: simplifiapi [-h] [--email [EMAIL]] [--token [TOKEN]] [--accounts]
44+
[--transactions] [--tags] [--categories]
45+
[--filename FILENAME] [--format {json,csv}]
46+
47+
simplifiapi — extract Quicken Simplifi data to JSON or CSV. Requires the
48+
SIMPLIFI_CLIENT_SECRET environment variable to be set (the OAuth client secret
49+
is no longer embedded in source — see README.md > Security & Trust Model).
50+
51+
options:
52+
-h, --help show this help message and exit
53+
--email [EMAIL] The e-mail address for your Quicken Simplifi account
54+
--token [TOKEN] Use existing token to bypass MFA check
55+
--accounts Retrieve accounts
56+
--transactions Retrieve transactions
57+
--tags Retrieve tags
58+
--categories Retrieve categories
59+
--filename FILENAME Write results to file with this prefix (path
60+
components stripped — see Security & Trust Model)
61+
--format {json,csv} The format used to return data.
62+
```
63+
64+
### Examples
1565
1666
```shell
17-
usage: simplifiapi [-h] [--email [EMAIL]] [--password [PASSWORD]] [--token [TOKEN]] [--accounts] [--transactions] [--tags] [--categories] [--filename FILENAME] [--format {json,csv}]
18-
19-
optional arguments:
20-
-h, --help show this help message and exit
21-
--email [EMAIL] The e-mail address for your Quicken Simplifi account
22-
--password [PASSWORD]
23-
The password for your Quicken Simplifi account
24-
--token [TOKEN] Use existing token to bypass MFA check
25-
--accounts Retrieve accounts
26-
--transactions Retrieve transactions
27-
--tags Retrieve tags
28-
--categories Retrieve categories
29-
--filename FILENAME Write results to file this prefix
30-
--format {json,csv} The format used to return data.
31-
32-
examples:
33-
> simplifiapi --token="..." --transactions
34-
> simplifiapi --token="..." --transactions --filename=20231125 --format=csv
67+
# Interactive login: getpass prompts for password (and MFA code if challenged).
68+
SIMPLIFI_CLIENT_SECRET=... simplifiapi --email=you@example.com --transactions
69+
70+
# Non-interactive: password from env, scrape to CSV with a date prefix.
71+
SIMPLIFI_CLIENT_SECRET=... SIMPLIFI_PASSWORD=... \
72+
simplifiapi --email=you@example.com --transactions --filename=20251125 --format=csv
73+
74+
# Reuse an existing bearer token (skip OAuth + MFA).
75+
SIMPLIFI_CLIENT_SECRET=... simplifiapi --token="..." --accounts --transactions
3576
```
3677
78+
Output files are written to the current working directory as `{filename}_{resource}.{format}` (e.g. `output_transactions.csv`). Any path separators or `..` segments in `--filename` are stripped before the file is opened.
79+
80+
### Exit codes & errors
81+
82+
The CLI catches `SimplifiAPIError` (and its `AuthenticationError` subclass) at the boundary and exits with a one-line `simplifiapi: <message>` instead of a Python traceback. Missing `SIMPLIFI_CLIENT_SECRET` is treated as a developer-environment error and fails loudly with a `RuntimeError` so it is not silently miscategorised as an auth failure.
83+
84+
### Logging
85+
86+
Set `LOG_LEVEL=DEBUG` to see the per-request pagination log. Bearer tokens, refresh tokens, MFA channels, and user IDs are never logged.
87+
3788
## Python API
3889
39-
The `Client` class allows accessing from python script and making custom analysis.
90+
The `Client` class lets you call the same endpoints from your own scripts. Importing the package is logging-pure — it attaches only a `NullHandler` to the `simplifiapi` logger and does not touch root-level logging.
4091
4192
```python
93+
import os
94+
from simplifiapi import AuthenticationError, SimplifiAPIError
4295
from simplifiapi.client import Client
4396

97+
# SIMPLIFI_CLIENT_SECRET must be set in the environment before calling get_token.
98+
# When calling Client directly, your code passes `password=` explicitly —
99+
# resolve it however you want (getpass, keyring, etc.), just don't read it
100+
# from sys.argv.
101+
os.environ["SIMPLIFI_CLIENT_SECRET"] = "<your-client-secret>"
102+
44103
client = Client()
45104

46-
# Provide either token or email/password
47-
token = "..."
48-
token = client.get_token(email=options.email, password=options.password)
105+
try:
106+
# Option A: full OAuth + (optional) MFA flow.
107+
token = client.get_token(email="you@example.com", password="<resolved-via-env-or-getpass>")
108+
109+
# Option B: reuse an existing bearer token instead of get_token.
110+
# token = "..."
111+
112+
# verify_token raises AuthenticationError on a bad token and installs the
113+
# bearer header on the session on success. It does not return a value.
114+
client.verify_token(token)
115+
116+
# Datasets own transactions and accounts.
117+
datasets = client.get_datasets()
118+
if not datasets:
119+
raise SystemExit("No datasets found for this account")
120+
dataset_id = datasets[0]["id"]
121+
122+
# All four getters return list[dict] of fully-unpaginated resources.
123+
transactions = client.get_transactions(dataset_id)
124+
accounts = client.get_accounts(dataset_id)
125+
tags = client.get_tags(dataset_id)
126+
categories = client.get_categories(dataset_id)
127+
except AuthenticationError as exc:
128+
# OAuth / token-verify failure. Typed subclass of SimplifiAPIError.
129+
raise SystemExit(f"auth failed: {exc}")
130+
except SimplifiAPIError as exc:
131+
# Pagination, JSON-decode, or unsafe nextLink rejection.
132+
raise SystemExit(f"api error: {exc}")
133+
```
134+
135+
### Exception hierarchy
136+
137+
Both exception types are re-exported from the package root:
49138
50-
assert client.verify_token(token)
139+
- `SimplifiAPIError` — base class for all boundary errors raised by `Client` (bad HTTP status on a resource fetch, invalid JSON in a response, refusal to follow an unsafe `nextLink`).
140+
- `AuthenticationError` (subclass of `SimplifiAPIError`) — OAuth authorize/token failure, MFA mismatch, or `verify_token` failure.
51141
52-
# Datasets own transactions and accounts
53-
datasets = client.get_datasets()
54-
datasetId = datasets[0]["id"]
142+
`RuntimeError` is raised separately when `SIMPLIFI_CLIENT_SECRET` is missing; it is intentionally not a `SimplifiAPIError` so the CLI does not catch and prettify it.
55143
56-
# Access transactions
57-
transactions = client.get_transactions(datasetId)
144+
## Development
145+
146+
```shell
147+
pip install -e '.[dev]'
148+
pytest # run the test suite
149+
pytest --cov # with coverage (gate is fail_under=80)
150+
pytest -k unit # only unit-marked tests
58151
```
59152
153+
The test suite uses `pytest` + the `responses` library to mock the Quicken HTTP surface. Tests live under [tests/](tests/).
154+
60155
## Thanks
61156
62-
This library is heavily inspired by [mintapi](https://github.com/mintapi/mintapi).
157+
This library is heavily inspired by [mintapi](https://github.com/mintapi/mintapi).

pyproject.toml

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,70 @@
11
[build-system]
2-
requires = ["setuptools"]
2+
requires = ["setuptools>=68"]
33
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "simplifiapi"
7+
version = "0.1.0"
8+
description = "An unofficial CLI and Python library for extracting Quicken Simplifi accounts, transactions, tags, and categories to JSON or CSV."
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
license = { text = "MIT" }
12+
authors = [
13+
{ name = "simplifiapi fork maintainers" }
14+
]
15+
keywords = ["simplifi", "quicken", "finance", "cli", "extraction"]
16+
classifiers = [
17+
"Development Status :: 4 - Beta",
18+
"Environment :: Console",
19+
"Intended Audience :: Developers",
20+
"Intended Audience :: End Users/Desktop",
21+
"License :: OSI Approved :: MIT License",
22+
"Operating System :: OS Independent",
23+
"Programming Language :: Python :: 3",
24+
"Programming Language :: Python :: 3.9",
25+
"Programming Language :: Python :: 3.10",
26+
"Programming Language :: Python :: 3.11",
27+
"Programming Language :: Python :: 3.12",
28+
"Topic :: Office/Business :: Financial",
29+
"Topic :: Utilities",
30+
]
31+
dependencies = [
32+
"requests>=2.31,<3",
33+
"pandas>=2,<3",
34+
"configargparse>=1.7,<2",
35+
]
36+
37+
[project.urls]
38+
Homepage = "https://github.com/rijn/simplifiapi"
39+
Source = "https://github.com/rijn/simplifiapi"
40+
Issues = "https://github.com/rijn/simplifiapi/issues"
41+
42+
[project.scripts]
43+
simplifiapi = "simplifiapi.cli:main"
44+
45+
[project.optional-dependencies]
46+
dev = [
47+
"pytest>=7,<9",
48+
"pytest-cov>=4,<6",
49+
"responses>=0.23,<1",
50+
]
51+
52+
[tool.setuptools.packages.find]
53+
include = ["simplifiapi*"]
54+
exclude = ["tests*"]
55+
56+
[tool.pytest.ini_options]
57+
testpaths = ["tests"]
58+
addopts = "-ra --strict-markers"
59+
markers = [
60+
"unit: unit tests with mocked HTTP",
61+
]
62+
63+
[tool.coverage.run]
64+
source = ["simplifiapi"]
65+
branch = false
66+
67+
[tool.coverage.report]
68+
fail_under = 80
69+
show_missing = true
70+
skip_covered = false

setup.cfg

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
1-
[metadata]
2-
name = setuptools
3-
version = 0.0.1
4-
5-
[options]
6-
install_requires =
7-
configargparse
8-
pandas
9-
requests
10-
packages = find:
11-
12-
[options.entry_points]
13-
console_scripts =
14-
simplifiapi = simplifiapi.cli:main
15-
16-
[options.packages.find]
17-
exclude =
18-
tests
1+
# Project metadata has been migrated to pyproject.toml [project] (PEP 621).
2+
# This file is intentionally left empty; do not re-add `[metadata]`, `[options]`,
3+
# or `[options.entry_points]` here — they belong in pyproject.toml.

simplifiapi/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
"""simplifiapi — Quicken Simplifi extraction CLI (security-remediated fork).
2+
3+
Importing this package attaches a no-op log handler to the
4+
``simplifiapi`` logger (the stdlib library idiom) so that library use
5+
without a configured root logger does not emit "No handlers could be
6+
found" warnings. It
7+
deliberately does not configure log levels or attach any other handler
8+
— that is the host application's job (and is done by
9+
``simplifiapi.cli.main`` for the CLI entry point). See
10+
``simplifiapi/cli.py::main`` for runtime log configuration.
11+
"""
12+
113
import logging
214

3-
logging.getLogger("simplifiapi").setLevel(logging.INFO)
15+
from simplifiapi.exceptions import AuthenticationError, SimplifiAPIError
16+
17+
logging.getLogger("simplifiapi").addHandler(logging.NullHandler())
18+
19+
__all__ = ["AuthenticationError", "SimplifiAPIError"]

simplifiapi/__main__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from simplifiapi.cli import main
1+
"""Entry point for `python -m simplifiapi`.
22
3-
import logging
3+
All logging configuration lives in ``simplifiapi.cli.main``. This module
4+
is a thin delegating shim.
5+
"""
46

5-
logging.getLogger("simplifiapi").setLevel(logging.INFO)
7+
from simplifiapi.cli import main
68

79
if __name__ == "__main__":
810
main()

0 commit comments

Comments
 (0)