Autosave is armed by a load that failed
What it costs you
The flag that permits this component to write the user's state back is set in a `finally`, so a load that failed arms it exactly like one that succeeded — while the component is still holding the empty values it initialised with. Opening the page during a blip is enough to write those empties over whatever the user had saved, and the write looks like any other.
The defect, and the fix
Both samples are scanned as app/checklist/page.tsx.
Our test suite runs both through the scanner on every build: the first must be reported, the second must not.
"use client";
export function Checklist() {
const [overrides, setOverrides] = useState({});
const [stateLoaded, setStateLoaded] = useState(false);
useEffect(() => {
(async () => {
try {
const res = await fetch("/api/checklist");
const data = await res.json();
if (data.state) setOverrides(data.state.overrides);
} catch {
// Non-critical
} finally {
setStateLoaded(true);
}
})();
}, []);
const saveState = useCallback(() => {
fetch("/api/checklist", { method: "POST", body: JSON.stringify({ overrides }) });
}, [overrides]);
useEffect(() => {
if (stateLoaded) saveState();
}, [overrides, stateLoaded, saveState]);
}"use client";
export function Checklist() {
const [overrides, setOverrides] = useState({});
const [stateLoaded, setStateLoaded] = useState(false);
const [stateLoadFailed, setStateLoadFailed] = useState(false);
useEffect(() => {
(async () => {
try {
const res = await fetch("/api/checklist");
if (!res.ok) throw new Error(`checklist load failed: HTTP ${res.status}`);
const data = await res.json();
if (data.state) setOverrides(data.state.overrides);
setStateLoaded(true);
} catch {
setStateLoadFailed(true);
}
})();
}, []);
const saveState = useCallback(() => {
fetch("/api/checklist", { method: "POST", body: JSON.stringify({ overrides }) });
}, [overrides]);
useEffect(() => {
if (stateLoaded && !stateLoadFailed) saveState();
}, [overrides, stateLoaded, stateLoadFailed, saveState]);
}What changed: The arming flag is set on the success path only and the failure is recorded separately, so a load that threw leaves autosave disarmed and the saved state untouched.
How to fix it
Set the arming flag on the success path only, and record the failure in its own state. The component can stay readable and interactive; it just must not write anything back until a load actually succeeds.
Why this rule doesn't cry wolf
Each clause below exists because it was attacked: someone was asked to find correct code that the rule would flag, and the clause is what stopped it. This is published because a check you cannot audit is a check you have to take on faith.
Fires on the ARMING, not on a missing guard: the shipped defect had a guard (`if (stateLoaded && !stateLoadFailed) saveState()`), and a rule keyed on unguarded writes would have missed it while reporting every correct autosave. Requires all of: a client file; a `useEffect` whose body reaches a persistent write (fetch with POST/PUT/PATCH, or localStorage/sessionStorage.setItem) either directly or through a same-file callback it names; that write gated on an identifier; a `set<Identifier>` for that same identifier called inside a `finally` block; and at least one `useState` in the file initialised to an empty literal — `[]`, `{}` or `""`, never `0`, because the empty literal is what turns "wrote too early" into "erased what was there". The join between the finally-set flag and the write's guard is the whole narrowing: `finally { setLoading(false) }` is everywhere and correct, and is only reportable when that same flag is what permits the write.
This is not a security finding
QA010 is reported in its own section, separately from security findings. It does not change your security grade, and it does not fail your build unless you pass --fail-on-quality. The security catalogue lives at /rules.
Other Correctness checks
Check your own code
npx xploitscan scan .Runs on every plan, including free. All 12 quality checks.