Skip to content

bug: validate wallet top-up amount and card before crediting balance - #340

Open
devin-ai-integration[bot] wants to merge 1 commit into
developfrom
devin/1789000466-wallet-topup-validation
Open

devin-ai-integration[bot] wants to merge 1 commit into
developfrom
devin/1789000466-wallet-topup-validation

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 10, 2026

Copy link
Copy Markdown

Description

Fixes a payment bypass in PUT /rest/wallet/balance (routes/wallet.ts addWalletBalance): the handler credited the wallet by the raw client-supplied req.body.balance after only checking that the caller owned some saved card. Since saved cards are never charged and the wallet balance is accepted as payment at checkout (routes/order.ts, paymentId === 'wallet'), any customer could grant themselves unlimited store credit.

Changes:

  • parseTopUpAmount() — the amount must be an integer in [10, 1000] (the same bounds the wallet UI already enforces client-side); anything else (negative, huge, fractional, non-numeric, missing) → 400. The server-parsed amount is what is credited and echoed back, never req.body.balance.
  • paymentId is coerced to a positive integer before the card lookup; an expired card (expYear/expMonth in the past) is rejected with 402 like a foreign/missing card.
  • API tests in test/api/wallet.test.ts for negative, over-max (and balance unchanged), non-numeric and fractional amounts.

Juice Shop has no real payment processor, so a genuine charge cannot be recorded; this fix bounds the exposure to the per-request limits the product already advertises. Seed cards all have expiry years ≥ 2081 so existing tests/e2e flows are unaffected.

Resolved or fixed issue: none

AI Tool Disclosure

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: Devin
    • LLMs and versions: Devin (Cognition AI)
    • Prompts: Fix code-scan finding: payment bypass via unvalidated req.body.balance in routes/wallet.ts addWalletBalance

Affirmation

Devin-Org: engineering


Devin Review

Signed-off-by: Devin AI <devin-ai-integration[bot]@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Devin Review

Comment thread routes/wallet.ts
Comment on lines +15 to +16
if (!Number.isInteger(amount) || amount < MIN_TOP_UP_AMOUNT || amount > MAX_TOP_UP_AMOUNT) {
return null

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🟡 Fractional wallet deposits fail after card selection

When balanceControl accepts a fractional amount, parseTopUpAmount rejects it after card selection. Users traverse the payment flow before learning that the accepted deposit cannot complete.

Learn more

The wallet form defines only required, minimum, and maximum validators. Angular therefore treats values such as 10.5 as valid and enables the continue button. The payment page converts the stored value with parseFloat and submits it through choosePayment. The new API integer check then returns 400, despite both client screens allowing the user to proceed.

Example: A user enters 10.5. The deposit button enables, card selection succeeds, and the final request returns “Top-up amount must be a whole number” instead of adding the displayed amount.

Recommended fix: Add an integer validator to balanceControl and display its validation error before navigation. Keep the frontend and API bounds and integer contract synchronized.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/wallet.ts
try {
await WalletModel.increment({ balance: req.body.balance }, { where: { UserId: req.body.UserId } })
res.status(200).json({ status: 'success', data: req.body.balance })
await WalletModel.increment({ balance: amount }, { where: { UserId: req.body.UserId } })

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🟨 Repeated top-ups bypass the credit cap

A card owner can repeat valid addWalletBalance requests without limit. Each request credits uncharged funds, so the wallet remains unbounded.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

0 participants