Skip to content

Full scan: 28 fixes across security, imports, transactions, accounts, budget and routes - #3

Merged
flaxvin merged 19 commits into
mainfrom
claude/beautiful-gates-1nevt6
Sep 27, 2026
Merged

flaxvin merged 19 commits into
mainfrom
claude/beautiful-gates-1nevt6

Conversation

@flaxvin

@flaxvin flaxvin commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Summary

A full scan of the app in six parallel audits: security, imports, transactions, accounts/loans/assets, the budget engine, and every route. Each defect below was reproduced with a test that fails on main, then fixed at the cause. The combined branch is clean on typecheck and passes 2117/2117 tests (main has 2076).

Three problems matter most:

  1. Access and privacy. A removed member's API token kept full access. A CAS import could write one member's folio into another member's private holding. Reports showed another member's envelope spending.
  2. Silent data loss or corruption. Imports silently dropped rows when the bank's reference column held "-", or failed outright with a 500. A loan payment with a negative half increased the debt. Deleting an envelope that a "call it even" was spent from broke the household's books from that month on.
  3. Denial of service. A PDF of a few hundred bytes could stall the server for minutes, through upload or email.

Findings (most severe first)

Sev Area Defect Fix
high security A removed member's API token kept full read/write access (e.g. /export.json) the bearer path checks the member is still in the household
high security A few-hundred-byte PDF expanded to gigabytes and held the single-threaded server for more than 2 minutes. Reachable via /import/pdf, /portfolio/cas and the email fetch 32 MB decompression budget per document
blocking imports A CAS import filed Ravi's folio into Priya's private holding of the same fund picks only holdings the importer can see
blocking imports Two identical rows plus an auto-approve rule made the whole import fail (500, foreign key) auto-approved rows join dedupe under their transaction id
blocking imports Rows silently dropped when the reference column held "-" or "0000…" (CSV and PDF) placeholder references are treated as no reference
blocking loans A payment split as −₹5,000 principal / ₹15,000 interest was accepted, and the debt went up negative halves and overpayment refused
blocking budget Deleting the envelope a "call it even" was spent from broke the giving budget's identity from that month on counted as history: refused without a remap, moved with one, undoable
blocking transactions The 30-day purge of deleted rows failed on every run once an imported row was deleted, and later housekeeping never ran purges row by row and releases import pointers
medium security Login CSRF on Google/OIDC: the sign-in state was not tied to the browser that started it a pathayam_oauth cookie must match the state
medium security While impersonating with writes on, POST /tokens minted a never-expiring token for the impersonated member refused (403)
should-fix privacy Another member's spending (their envelope, on a household-visible account) appeared in the tag chart, heatmap, trend and FIRE living costs hiddenTransactionSql in reports and envelopeSpendBetween
should-fix privacy A transfer from a private account wrote "Transfer from ‹private account name›" on the joint account's leg that leg now says "a private account" (existing rows keep the old memo)
should-fix tax 25%/37% surcharge was applied when only 111A/112/112A gains pushed income over ₹2 crore/₹5 crore band tested on income excluding those gains. Example: ₹1.5 crore slab + ₹1 crore 111A gives ₹72,71,680 (was ₹76,96,000)
should-fix accounts Tracking-account reconciliation moved household Ready to Assign adjustment kept off budget
should-fix accounts An empty budget_id on account edit dropped the account out of every budget refused
should-fix accounts Statement/due days of 45, −3, 0 and 2.5 were stored limited to 1–31
should-fix net worth A past-date net worth used a hand-valued asset's later valuation valued as of that date
should-fix loans A payment larger than the outstanding left the loan account in credit refused
should-fix assets Negative purchase fees reduced a lot's cost refused
should-fix transactions A refused change still marked the reconciliation checkpoint broken break happens in the same transaction as the change
should-fix transactions An edit with date 31-02-2026 kept the old date and said "Saved." 400
should-fix transactions A memo-only edit on one leg of an imported transfer moved the other leg's date other leg moves only when its value actually changes
should-fix imports "1 to review" notice when every row was auto-approved counts only what is still in Review
should-fix routes /schedules?days=abc returned 500; ?days=2000000 took about 5 s, then 500 falls back to 60 days; over 3,650 refused (422)
should-fix routes "Pay it off" on Cards passed paise and /transfer ignored it, so the form opened empty carries rupees and the form is prefilled
low security A JSON body with an object value produced a 500 (named-parameter binding). Reachable anonymously on the demo JSON bodies reduced to form shape
nit routes "Nothing due in the next -1 days" fixed with the horizon validation

Coverage and what was clean

  • Routes: all 208 exercised, about 42,000 requests. Every GET route was crawled as each of the 4 sim members. Every POST was sent as rendered, empty, and with 19 boundary/garbage values per field. No other 500s, no unescaped markup (the stored XSS probe was always escaped), and no dead links or forms.
  • Budget engine fuzz: randomized operation sequences on 40 seeds (300–500 steps each) across household and personal budgets. After every step: identity is 0 in every scope, and the rollup matches a cold compute.
  • Security, found sound: CSRF origin checks, safePath redirects, session cookie flags and re-checking on every request, password lockout parity, OIDC issuer/audience/email_verified, output escaping, CSV formula guard, attachment handling, CSP/HSTS/no-store, production gates on dev and demo modes.
  • Maths checked by hand: EMI and interest schedules, new- and old-regime tax with the 87A rebate, and capital gains rates.

Not in this PR (decisions or follow-ups)

  • "Reduce tenure" prepayment changes the EMI. Example: ₹8,884.88 → ₹8,418.75 on ₹1L/12%/12 months after ₹20,000 prepaid, yet the message says the instalment is unchanged. Keeping the instalment needs a schema column to store it.
  • Nits read from code, not reproduced:
    • A crafted rule with op=oneOf and a string value would 500 later imports.
    • A deleted transaction can still be edited.
    • Deleting a card charge that was converted to EMI is not refused.
    • POST /portfolio/refresh can block for about 55 s when the price providers are unreachable.
    • The 80D ceiling for a senior-parent combination is simplified.

🤖 Generated with Claude Code

https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu


Generated by Claude Code

…owed

A principal of -5,000 beside 15,000 of interest added up to a 10,000
payment and raised the debt; a 2,00,000 instalment against 1,00,000
floored the outstanding at nil while the loan account went into credit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…onth

An empty budget_id wrote NULL onto a budget account, taking its balance
out of every budget's Ready to Assign; statement and due days of 45, -3
or 2.5 were stored as typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
Filed to the household's Reconciliation envelope, bringing a fixed
deposit up by 100 to the bank's figure lowered Ready to Assign by 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…ve fees

A dated net worth statement read the latest valuation whatever its date,
so a September revaluation landed in June's snapshot. A purchase with
negative fees was booked below what the units cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
A called-even amount is spent from an envelope with no transaction behind
it, so an envelope that held nothing and had nothing filed to it could be
deleted while even_calls still named it. The engine stopped reading it and
the giving budget's identity was out by the amount from that month on.
Without a remap the delete is now refused; with one the calls follow it,
and undoing the delete brings them back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…ains

The Act tests those bands on total income excluding 111A, 112A and 112
gains; income over ₹2 crore only because of them takes 15%. Testing total
income put ₹1.5 crore of salary plus a ₹1 crore 111A gain in the 25% band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…s envelope

These reports filtered on the account's visibility only, so spending on a
household-visible account in Ravi's own budget, filed to his own envelope,
reached Priya's /reports tag chart and heatmap and her FIRE living costs.
They now use hiddenTransactionSql, the account and the envelope, like
Query and the registers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
A doubly-Flate-compressed stream of a few hundred bytes inflated without limit,
and a page could list it any number of times: a sub-kilobyte upload or Gmail
statement attachment held the server for minutes and asked for gigabytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
An object value reached node:sqlite as named parameters and threw: a 500 and a
fault row per request from the public /demo/enter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…e impersonating, login CSRF

- A removed member's API token still read and wrote everything; the allow-list
  now applies to tokens as it does to sessions.
- Minting a token while viewing as another member created a permanent
  credential of theirs; refused.
- The OAuth/OIDC state is bound to the starting browser by a cookie, so a
  callback link cannot sign somebody else into the sender's account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…ceholder reference is no reference

Two identical orders in one file with an auto-approving rule failed the whole
import on a foreign key (the second row named the first's review item as the
transaction it duplicated). And a "-" or all-zero Ref No cell made different
rows one event by the strong tier, so the later row was 'upgraded' into
nothing and dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
`staged` includes auto-approved rows, so an all-auto-approved import read
"1 to review, 1 auto-approved" and opened an empty Review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
Every such row of one amount was the same event to the strong dedupe tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
planCasImport took the first holding of the instrument anywhere in the
household, so Ravi's folio of a fund Priya holds privately was recorded in
her private account. It now looks only at holdings the importer can see,
preferring the chosen destination's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
purgeDeleted ran one DELETE over every expired row, and an imported line
approved from Review and then deleted is still named by its staged row. The
foreign key failed the whole statement on every housekeeping tick, so no
deleted transaction was ever purged again and the steps after it never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…ad edit dates

guardCheckpoints broke confirmed checkpoints before the edit, add or delete
ran, so a change then refused (lines over the total, spending with no
envelope, a load-bearing delete) left a checkpoint broken over a balance
nothing had moved. The break now happens in the change's own transaction.

The edit route read an unparseable date as 'keep the old one' and answered
'Saved.' over the dropped change; it now refuses it as every other date
field does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
…name a private account on the other leg

updateTransaction rewrote the partner leg's amount and date whenever either
was posted, and the edit form posts both on every save. An imported transfer's
legs carry their own statements' dates, so a memo edit on the card side moved
the bank side out of its reconciled period with no question asked. The
partner now moves only when the value actually changes, and the edit route
guards the partner's checkpoint from the partner's own date.

createTransfer wrote 'Transfer from <other account>' on each leg, so the
household leg of a transfer with a private account named that account in the
other member's register, transaction page, Search and CSV export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
/schedules?days= went straight through Number(): 'abc' was a 500 (Invalid
time value), -5 or an empty value read 'the next -1/0 days', and two million
days overflowed the chart's stack after five seconds. Nonsense now falls back
to 60 days, and a horizon past ten years is a 422.

The Cards page's 'Pay it off' link carried the balance in paise and /transfer
never read it, so the form opened empty. The link now carries rupees and the
transfer form fills them in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zXWwzys3K4XksLh3rrcKu

flaxvin commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

The github-advanced-security check (run 36310637304) failed for the same reason as on #1 and #2. Nothing in this PR caused it.

GitHub's hosted AI scanner stopped before it read any file. It failed while opening its model session: CAPIError: 400 The requested model is not supported. No findings were produced. Nothing in this repository sets that model, so this PR can't fix it.

On #1 the API refused to re-run this kind of check (403 This workflow run cannot be retried), so I haven't re-run it here.

The PR's own checks on edd8e1d: npm run typecheck is clean and npm test passes 2117/2117.


Generated by Claude Code

@flaxvin
flaxvin marked this pull request as ready for review September 27, 2026 15:21
@flaxvin
flaxvin merged commit 4d3ae2e into main Sep 27, 2026
3 of 4 checks passed
febin92 added a commit that referenced this pull request Sep 27, 2026
The fixers updated the docs finding by finding; this pass catches what they
left stale elsewhere, and the website, which nothing had touched.

- website/api.html: status codes (403 for a read-only token, 422 for a
  reused idempotency key with a new body, 404 for a missing or hidden id),
  the real /query parameters, /import/map and its date-order choice, the
  PDF upload field, transfer fees, the token deny-list, and more.
- website/guides.html: three target kinds, fill-from-last-month, moves
  between budgets refused, import date order and Dr/Cr files, recording a
  card payment from Review, rules as the form really writes them, schedules
  recorded when marked paid, investments and tax changes, sessions as an
  absolute lifetime.
- demo and privacy pages: the demo resets on every wake and is re-seeded
  every Wednesday; sign-in is a password, OIDC or Google.
- docs: the error table (Missing is a 404, JSON errors), token rules, PDF
  decompression cap, privacy guards, rule amounts, screens, data model,
  testing counts; index updated.
- home page test count to 2,117; sitemap lastmod for the changed pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
febin92 pushed a commit that referenced this pull request Oct 5, 2026
Full scan: 28 fixes across security, imports, transactions, accounts, budget and routes
febin92 added a commit that referenced this pull request Oct 5, 2026
The fixers updated the docs finding by finding; this pass catches what they
left stale elsewhere, and the website, which nothing had touched.

- website/api.html: status codes (403 for a read-only token, 422 for a
  reused idempotency key with a new body, 404 for a missing or hidden id),
  the real /query parameters, /import/map and its date-order choice, the
  PDF upload field, transfer fees, the token deny-list, and more.
- website/guides.html: three target kinds, fill-from-last-month, moves
  between budgets refused, import date order and Dr/Cr files, recording a
  card payment from Review, rules as the form really writes them, schedules
  recorded when marked paid, investments and tax changes, sessions as an
  absolute lifetime.
- demo and privacy pages: the demo resets on every wake and is re-seeded
  every Wednesday; sign-in is a password, OIDC or Google.
- docs: the error table (Missing is a 404, JSON errors), token rules, PDF
  decompression cap, privacy guards, rule amounts, screens, data model,
  testing counts; index updated.
- home page test count to 2,117; sitemap lastmod for the changed pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants