Build1 distinct publisher3 min readUpdated
A dev.to walkthrough sends {"amount": NaN} to an ordinary Flask endpoint and drains it. Python's json parser accepts the constant, and three plausible guards all let it through.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A walkthrough published on dev.to takes an ordinary Flask withdrawal endpoint, wraps it in three checks that look complete, and then empties it with a single request body of `{"amount": NaN}` [1][4]. The guards test type and magnitude, and none of them asks whether the value is a usable number, which is exactly the gap Python's own JSON parser hands them [5].
The endpoint holds a balance of 1000 in a module-level dict, parses the body with `json.loads`, rejects anything that is not a JSON object, and then applies three tests: `isinstance(amount, (int, float))`, `amount <= 0`, and `amount > STATE["balance"]` [1]. The stated intent is the obvious one: accept numeric amounts, allow only positive withdrawals, refuse anything larger than the balance [2]. On the happy path it behaves. 100 leaves 900, 0 comes back as "too small", 9999999 is rejected as greater than the balance, 900 takes the balance to 0, and a repeat 900 is refused [3].
Then the post sends `{"amount": NaN}` and gets "Success! New balance: nan" [4]. Python's native `json` parser accepts NaN, described in the post as one of the non-standard JSON constants, and converts it to `float("nan")` [5][8]. That object is a float, so the `isinstance` guard admits it [14]. The remaining two guards are comparisons, and since the request succeeded, neither `amount <= 0` nor `amount > balance` was true of the value [15].
Arithmetic does the rest. `1000 - nan`, `1000 + nan`, `nan * 0` and `nan - nan` all evaluate to nan [6]. The balance is now nan, and every subsequent withdrawal succeeds: the post follows the NaN request with 100 and then 1000, both returning "Success! New balance: nan" [7]. That is 1100 withdrawn against a starting balance of 1000 with no guard tripped [16].
Persisting the value changes the failure mode rather than containing it. In the SQLite variant the balance is a REAL column seeded at 1000; a withdrawal of 123 writes 877.0, and the NaN withdrawal writes NULL [9][10]. The author's summary is that the balance is broken from then on and cannot be operated on [11]. Postgres, according to the same post, stores NaN as an actual numeric value, corrupting the balance in a different way [12].
The transferable point is the shape of the check rather than the specific constant. `isinstance` answers a question about the object's type, and float("nan") is a float [14]. Because nan propagates through arithmetic [6] and does not compare true against a threshold [15], no combination of a type test and relative magnitude tests can exclude it; the guard has to assert finiteness before the value reaches the subtraction [17]. Two things worth an audit in your own services: every path where a numeric field from `json.loads` is subtracted, compared or written to a column, and what your specific database does with a non-finite float, since SQLite and Postgres in this post diverge [10][12]. Note also that the source material available here cuts off mid-command after the SQLite NULL, so the Postgres behaviour is asserted by the author rather than shown [12].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
curl 0:5000/withdraw --json '{"amount": NaN}' returns "Success! New balance: nan".
The native json parser in Python accepts the NaN constant, converting it to float("nan") and allowing arithmetic operations on it.
Once NaN is passed it corrupts the state by replacing the balance with float("nan") and allows an unlimited number of withdrawals: after the NaN request, requests with amount 100 and amount 1000 both return "Success! New balance: nan".
The post refers to NaN as one of the non-standard JSON constants.
In the SQLite variant (withdraw_sqlite.py) the account table has a balance REAL column seeded with 1000; the database initially reads 1|1000.0, and a withdrawal of 123 returns "Success! New balance: 877.0" with the database then reading 1|877.0.
After a withdrawal of NaN against the SQLite-backed endpoint, the response is "Success! New balance: nan" and the stored balance is NULL (shown as 1| and, with .nullvalue NULL, as 1|NULL); the NaN was converted to a NULL value.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Reproducible transcripts, one publisher
The core behaviour is shown end to end with runnable code, curl requests and responses, an interpreter session, database queries and a real traceback, so it is checkable by any reader. Against that, everything comes from one dev.to post with no independent reproduction, and the Postgres half of the persistence claim is asserted without any transcript, while the supplied text ends mid-snippet in the mitigation section.
No adoption signal in sources
The cluster contains one tutorial-style post. It reports no releases, deployments, benchmarks, security incidents, disclosed usage, or pricing and licensing changes, and gives no indication of how often this pattern appears in real systems, so no adoption level can be measured without inferring facts the source does not supply.
Mildly ahead of what is shown
The headline mechanics are matched by the demonstrations, so the framing is close to aligned; the post does not claim a novel vulnerability class or real-world breach. The small positive gap comes from the Postgres behaviour being asserted rather than shown, and from the 'unlimited withdrawals' framing resting on a toy in-memory endpoint whose relevance to production stacks is not evidenced.
Low commercial incentive
The source is a personal technical post on a community publishing platform that markets no product, service, scanner, or paid offering; its remedy is a standard-library option (json.loads' parse_constant). The residual incentive is ordinary developer-audience attention for a striking result, which plausibly shapes the dramatic 'drains the endpoint' framing.
Mechanism solid, breadth unknown
Confidence in the demonstrated mechanism is high because it is reproducible from the listings and transcripts in minutes. Confidence in the wider story is moderate: one publisher, no independent verification, no evidence about prevalence in production code, and one undemonstrated database claim.
build
Six pragmas and a context manager: the vector store that fits in 2GB of RAM1 distinct publisher
build
The duplicate def that ate the trim, and the lint rule nobody was running1 distinct publisher
build
Force the tool call, then hand Lightsail a long-lived key1 distinct publisher
build
Stop timing your GraphQL tests and start counting loader calls1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 17, 2026