QA010highCorrectness

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.

Reported
"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]);
}
Not reported
"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.