Skip to content

Fix payment-callback security, clean up Android, harden formula parser#19

Merged
Leonard-Don merged 13 commits into
mainfrom
agent/codex-7proj-20260520-214920-android
May 21, 2026
Merged

Fix payment-callback security, clean up Android, harden formula parser#19
Leonard-Don merged 13 commits into
mainfrom
agent/codex-7proj-20260520-214920-android

Conversation

@Leonard-Don

Copy link
Copy Markdown
Owner

Summary

Two streams of work on this branch (13 commits off main).

Post-evaluation P0/P1/P2 fixes (b6e567f, 25674b6, 36e423d)

  • P0 backend security — payment callbacks now fail closed: verify_callback_signature requires a verified HMAC signature by default, so an unsigned callback can no longer forge a PAID event and grant VIP (explicit QUANTTRADING_REQUIRE_CALLBACK_SIGNATURE=0 opt-out for local/dev). sandbox_payment_callback runs under BEGIN IMMEDIATE, so two concurrent callbacks for one order can no longer both pass the status check and double-grant entitlements.
  • P2 backend — refresh tokens are single-use (rotated on refresh) and expire after 30 days; new POST /v1/auth/logout; dead sessions pruned on login; indexes added on orders(user_id), orders(provider_transaction_id), payment_callbacks(order_id).
  • P1 Android — removed the unused core/ offline-first layer (~630 lines, referenced only by itself + its test, never wired into production); replaced bare unsalted SHA-256 local password hashing with salted PBKDF2-HMAC-SHA256 (new PasswordHasher + PasswordHasherTest).
  • DocsREADME.md no longer over-claims "v1.0.0 / ready to ship" or "complete PIPL-compliant" (now consistent with COMMERCIALIZATION_GAP.md, with an explicit No-go list); SERVER_CONTRACT.md corrected to the real API (paymentPayload schema, callback timestamp, /v1/auth/logout).

Quant formula parser robustness (87d7f7c3ce9b36, 10 earlier commits)

  • Full-width character handling (spaces, parentheses, digit/decimal/percent literals, operators, reversed comparators) and clearer formula validation error messages.

Test plan

  • Backend — cd backend && python3 -m pytest -> 46 passing. 8 new tests cover: fail-closed callback default, the explicit opt-out, concurrent-callback double-grant, refresh-token rotation, refresh-token expiry, logout, and the sessions schema migration.
  • Android — cd QuantTradingApp && ./gradlew :app:testDebugUnitTestnot run; the dev environment has no JVM. Needs verification (covers PasswordHasherTest and the formula parser tests).
  • Android build — ./gradlew :app:assembleDebug.

🤖 Generated with Claude Code

Leonarddon and others added 13 commits May 20, 2026 22:08
Expose parser rejection reasons through the quant view model/UI and cover invalid formula feedback with focused policy tests.
Payment callbacks now fail closed: verify_callback_signature requires a
verified HMAC signature by default, so an unsigned callback can no
longer forge a PAID event and grant VIP. The unsigned path needs an
explicit QUANTTRADING_REQUIRE_CALLBACK_SIGNATURE=0 opt-out.

sandbox_payment_callback runs under BEGIN IMMEDIATE so two concurrent
callbacks for one order cannot both pass the status check and
double-grant entitlements.

Sessions: refresh tokens are single-use (rotated on refresh) and expire
after 30 days; add POST /v1/auth/logout; prune dead sessions on login;
index orders(user_id), orders(provider_transaction_id) and
payment_callbacks(order_id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete the core/ offline-first layer (OfflineFirstRepository,
RepositoryResponse, CacheEntry, DataProvenance, ProviderHealthSnapshot,
DataOrigin) and its test -- ~630 lines referenced only by each other,
never wired into production code.

Replace the local-account password hash: bare unsalted SHA-256 becomes
salted PBKDF2-HMAC-SHA256 (120k iterations) in a new PasswordHasher,
covered by PasswordHasherTest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README: drop the "v1.0.0 / waiting-for-store" and "complete
PIPL-compliant" claims that contradicted COMMERCIALIZATION_GAP.md, add
an explicit No-go list, and stop describing SQLite as "Postgres-ready".

SERVER_CONTRACT.md: correct paymentPayload to the real
{sandbox,orderId,channel,note} shape, add the callback timestamp field,
document POST /v1/auth/logout and refresh-token rotation, and fix the
access-token / refresh-response examples.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36e423ddc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/app/main.py
Comment on lines 399 to 401
"SELECT * FROM sessions WHERE refresh_token = ? AND device_id = ?",
(refresh_token, device_id),
).fetchone()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make refresh-token rotation atomic under concurrent requests

refresh_session reads the session row first and revokes it later, so two concurrent refresh calls with the same refresh_token/device_id can both pass the SELECT before either DELETE commits; the second DELETE becomes a no-op but still returns a new session, which breaks the intended single-use guarantee. This is exploitable specifically when a token is replayed in parallel (e.g., leaked token used by attacker and client at the same time).

Useful? React with 👍 / 👎.

Comment thread backend/app/main.py
columns = {row["name"] for row in conn.execute("PRAGMA table_info(sessions)")}
if "refresh_expires_at" not in columns:
conn.execute(
"ALTER TABLE sessions ADD COLUMN refresh_expires_at INTEGER NOT NULL DEFAULT 0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Backfill refresh_expires_at when migrating sessions

Adding refresh_expires_at with DEFAULT 0 causes all pre-migration session rows to look already expired (0 < now_millis()), so users with existing sessions will get 401 refresh token expired as soon as they need refresh after deploy. This creates an avoidable mass re-login event on upgrade; migration should initialize legacy rows to a non-expired value (or derived expiry) instead of zero.

Useful? React with 👍 / 👎.

Comment on lines +157 to +158
val matched = state.phone == phone &&
state.passwordHash?.let { PasswordHasher.verify(password, it) } == true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept and migrate legacy local password hashes

Offline login now only validates passwordHash through PasswordHasher.verify, which expects the new salt:digest format; hashes created by previous app versions (plain SHA-256 string) will always fail verification, so existing local users cannot log in when backend auth is unavailable. This regression affects upgraded installs with preexisting local credentials unless legacy format checking/migration is retained.

Useful? React with 👍 / 👎.

@Leonard-Don Leonard-Don merged commit 448ffb3 into main May 21, 2026
1 check passed
@Leonard-Don Leonard-Don deleted the agent/codex-7proj-20260520-214920-android branch May 21, 2026 05:37
Leonard-Don added a commit that referenced this pull request May 26, 2026
#19)

* fix(quant): surface formula validation errors

Expose parser rejection reasons through the quant view model/UI and cover invalid formula feedback with focused policy tests.

* fix: explain unsupported formula punctuation

* fix: explain full-width formula operators

* fix: explain reversed formula comparators

* fix: explain percent formula literals

* fix: explain full-width percent literals

* fix: explain full-width digit literals

* fix: explain full-width decimal literals

* fix: explain full-width formula parentheses

* fix: allow full-width formula spaces

* fix(backend): harden payment callbacks and session lifecycle

Payment callbacks now fail closed: verify_callback_signature requires a
verified HMAC signature by default, so an unsigned callback can no
longer forge a PAID event and grant VIP. The unsigned path needs an
explicit QUANTTRADING_REQUIRE_CALLBACK_SIGNATURE=0 opt-out.

sandbox_payment_callback runs under BEGIN IMMEDIATE so two concurrent
callbacks for one order cannot both pass the status check and
double-grant entitlements.

Sessions: refresh tokens are single-use (rotated on refresh) and expire
after 30 days; add POST /v1/auth/logout; prune dead sessions on login;
index orders(user_id), orders(provider_transaction_id) and
payment_callbacks(order_id).

* refactor(android): remove dead offline layer, salt password hashes

Delete the core/ offline-first layer (OfflineFirstRepository,
RepositoryResponse, CacheEntry, DataProvenance, ProviderHealthSnapshot,
DataOrigin) and its test -- ~630 lines referenced only by each other,
never wired into production code.

Replace the local-account password hash: bare unsalted SHA-256 becomes
salted PBKDF2-HMAC-SHA256 (120k iterations) in a new PasswordHasher,
covered by PasswordHasherTest.

* docs: align README with gap docs, correct server contract

README: drop the "v1.0.0 / waiting-for-store" and "complete
PIPL-compliant" claims that contradicted COMMERCIALIZATION_GAP.md, add
an explicit No-go list, and stop describing SQLite as "Postgres-ready".

SERVER_CONTRACT.md: correct paymentPayload to the real
{sandbox,orderId,channel,note} shape, add the callback timestamp field,
document POST /v1/auth/logout and refresh-token rotation, and fix the
access-token / refresh-response examples.

---------
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