mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(folio): an amount over the app's cap spends no window budget
the corpus reaches TxnSubmit with 999999999999999999999, AMOUNT_REGEX takes it and AddTransactionViewModel refuses it against MAX_TRANSACTION_AMOUNT_CENTS, so counting it was budget a double submit could hide behind.
This commit is contained in:
1 parent
dfa3660659
commit
a3ed4608da
2 files changed
+35
-6
No files matched your search
@@ -283,6 +283,9 @@ export function countSubmitsInWindow(args: {
|
|||||||
return { reported, next: fresh ? 0 : reported };
|
return { reported, next: fresh ? 0 : reported };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Folio's own cap, in cents (core/data/Repository.kt).
|
||||||
|
const MAX_TRANSACTION_AMOUNT_CENTS = 100_000_000;
|
||||||
|
|
||||||
// Could the app have committed anything for that submit? The amount field as
|
// Could the app have committed anything for that submit? The amount field as
|
||||||
// the LANDING frame shows it is the form state the tap read: the tap changes
|
// the LANDING frame shows it is the form state the tap read: the tap changes
|
||||||
// nothing about it, and one action runs per step, so nothing else could have.
|
// nothing about it, and one action runs per step, so nothing else could have.
|
||||||
@@ -302,14 +305,19 @@ export function countSubmitsInWindow(args: {
|
|||||||
// nothing to say. Measured over four recorded android runs, 19, 11, 25 and 25
|
// nothing to say. Measured over four recorded android runs, 19, 11, 25 and 25
|
||||||
// of 35, 26, 42 and 42 submit taps landed with the amount field empty.
|
// of 35, 26, 42 and 42 submit taps landed with the amount field empty.
|
||||||
//
|
//
|
||||||
// An amount too large for a Kotlin Long is refused by the app too, and still
|
// An amount over Folio's cap is refused before any coroutine starts
|
||||||
// counts here: over-counting can only cost a detection, and the reading that
|
// (MAX_TRANSACTION_AMOUNT_CENTS, checked in both AddTransactionViewModel.submit
|
||||||
// would have to prove the overflow is a float that cannot hold the number.
|
// and Repository.createTransaction), and the fuzzer's corpus reaches the button
|
||||||
|
// with one: "999999999999999999999" passes AMOUNT_REGEX, so the field takes it.
|
||||||
|
// Float is precise enough to say which side of the cap an amount is on. The cap
|
||||||
|
// is 1e8, every integer cent up to 2^53 is exact, and an amount far enough above
|
||||||
|
// it to be inexact is far enough above it to be refused.
|
||||||
export function submitCouldCommit(amountText: string | undefined): boolean {
|
export function submitCouldCommit(amountText: string | undefined): boolean {
|
||||||
if (amountText === undefined) return true;
|
if (amountText === undefined) return true;
|
||||||
const trimmed = amountText.trim().replace(/,/g, "");
|
const trimmed = amountText.trim().replace(/,/g, "");
|
||||||
if (!/^\d+(\.\d{1,2})?$/.test(trimmed)) return false;
|
if (!/^\d+(\.\d{1,2})?$/.test(trimmed)) return false;
|
||||||
return /[1-9]/.test(trimmed);
|
if (!/[1-9]/.test(trimmed)) return false;
|
||||||
|
return Number(trimmed) * 100 <= MAX_TRANSACTION_AMOUNT_CENTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses formatCents output like "$5.00", "-$1,234.56", "+$0.50" back to
|
// Parses formatCents output like "$5.00", "-$1,234.56", "+$0.50" back to
|
||||||
|
|||||||
@@ -89,11 +89,32 @@ test("a submit the app must have refused does not spend the window's budget", ()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Folio caps a transaction at $1,000,000.00 (MAX_TRANSACTION_AMOUNT_CENTS, in
|
||||||
|
// core/data/Repository.kt), and AddTransactionViewModel.submit refuses anything
|
||||||
|
// over it before a coroutine starts. The fuzzer's corpus carries
|
||||||
|
// "999999999999999999999", AMOUNT_REGEX lets it into the field and it reaches
|
||||||
|
// the button, so this is a refusal the window used to pay for.
|
||||||
|
test("an amount over the app's cap cannot commit", () => {
|
||||||
|
for (const amountText of ["1000000.01", "1,000,001", "999999999999999999999"]) {
|
||||||
|
assert.deepEqual(
|
||||||
|
countSubmitsInWindow({
|
||||||
|
previousCount: 0,
|
||||||
|
lastAction: { kind: "Tap", on: submitOn },
|
||||||
|
amountText,
|
||||||
|
fresh: false,
|
||||||
|
}),
|
||||||
|
{ reported: 0, next: 0 },
|
||||||
|
`amount ${JSON.stringify(amountText)} was counted as a possible commit`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// The field as the landing frame shows it, which is the form state the tap read:
|
// The field as the landing frame shows it, which is the form state the tap read:
|
||||||
// nothing between the two changes it. Anywhere but the transaction screen there
|
// nothing between the two changes it. Anywhere but the transaction screen there
|
||||||
// is no field to read, and unknown has to count.
|
// is no field to read, and unknown has to count. The cap itself is an amount the
|
||||||
|
// app takes, so it counts too.
|
||||||
test("an amount that could commit, or that nobody could read, spends the budget", () => {
|
test("an amount that could commit, or that nobody could read, spends the budget", () => {
|
||||||
for (const amountText of ["5", "0.01", "1,000", "999999999999999999999", undefined]) {
|
for (const amountText of ["5", "0.01", "1,000", "1000000.00", "999999.99", undefined]) {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
countSubmitsInWindow({
|
countSubmitsInWindow({
|
||||||
previousCount: 0,
|
previousCount: 0,
|
||||||
|
|||||||
Reference in new issue
Block a user