- _redact_credentials: blank scalar descendants of objects reached
through a credential-owned list (e.g. tokens: [{"value": "secret"}])
regardless of field name -- the existing name-based walk only
protected direct dict values under a credential key, not list items.
- wifi.py: reject enabled/force/auto_enable_ap_mode values
_parse_bool_ish can't recognize (400) instead of silently treating
them as False, which could disable Wi-Fi or the radio itself.
- Starlark manifest locking: lock a stable manifest.json.lock sidecar
instead of manifest.json itself, in both the standalone route path
(_starlark_manifest_lock) and the plugin path
(StarlarkAppsPlugin._save_manifest / _update_manifest_safe).
manifest.json is replaced by an atomic rename on every write, which
swaps in a fresh inode; a lock held on the old inode does not
exclude a second locker that opens the path afresh right after the
rename and gets the new inode, so two writers could race despite
each holding "a lock". A sidecar that no write ever touches always
resolves to the same inode for every locker.
Skipped as stale: the "serialize the complete manifest
read-modify-write" finding at api_v3/__init__.py -- every standalone
handler that calls _write_starlark_manifest is already wrapped in
_starlark_manifest_lock() on this branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeRabbit's review of the blueprint split (#553) asked that
POST /backup/restore reject option keys outside RestoreOptions'
known set. The follow-up commit fixed the bool("false")-is-True
bug with _coerce_to_bool but never added the key check: a typo'd
or renamed key (e.g. "restoreSecrets") is silently ignored by
opts_dict.get(key, True), so the flag stays at its True default
and secrets get restored despite the caller's request saying
otherwise -- with no indication anything was wrong.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vmcwf5vMgYqdt8bJTZtiwb
Fixes to the api_v3 package split (PR #553), one per finding verified
against the actual code:
- __init__.py: _redact_credentials only blanked scalar values under a
credential-named key; a bare list of secrets under such a key (e.g.
tokens: ["a", "b"]) passed through untouched, since the list branch
recursed with no memory that its key looked like a credential. Nested
dicts still walk normally (a documented, tested behaviour -- a container
like secrets: {api_key: ..., note: ...} is a section name, not a value to
blank outright), but any value reached under a credential-shaped key is
now actually blanked.
- __init__.py: the OAuth helper script's raw stderr/stdout went to
logger.error unredacted (CWE-532) right next to a comment claiming this
was deliberate; the HTTP response already used the existing redact_text
helper. Routed the log line through the same helper.
- __init__.py / starlark.py: the standalone Starlark manifest fallback
(used when the plugin instance isn't loaded) read-modified-wrote
manifest.json with no lock, unlike StarlarkAppsPlugin._update_manifest_safe
(plugin-repos/starlark-apps/manager.py), which already holds an flock for
the same file when the plugin is loaded. Added _starlark_manifest_lock,
mirroring that pattern, and wrapped every standalone read-modify-write
call site in it. The app-config update route also wrote config.json and
the manifest as two separate, non-transactional writes (a second,
distinct finding at the same call site); config.json is now rolled back
if the manifest write that follows it fails.
- backup.py: restore options used bare bool() on values from the request,
so {"restore_secrets": "false"} restored secrets anyway (bool("false") is
True). Switched to the existing _coerce_to_bool helper already used for
this exact purpose elsewhere in the package.
- config.py: an automated import-rewrite mangled four user-facing
validation strings and their neighbouring comments -- "Invalid start
time" had become "Invalid start _pkg.time" (and likewise for "end time")
in both the schedule and dim-schedule per-day validation paths.
- display.py: `import _pkg.time as time_module` -- _pkg is a local alias
for the package, not a real importable module, so this raised
ModuleNotFoundError whenever a caller restarted an already-running
display service via /display/on-demand/start, after the on-demand
request was already written to cache. Fixed to `import time`. Audited
the rest of the package for the same `_pkg.<module>` import mistake;
every other `_pkg.` reference is a legitimate attribute read-through
(`_pkg.time.time()`, `_pkg._get_starlark_plugin()`, ...), not a broken
import statement.
- fonts.py: validate_file_upload's max_size_mb parameter is silently
unused by that helper (it only checks filename/extension) -- the font
upload route saved arbitrarily large files as a result. Added the same
seek-and-check pattern already used for the sibling .star upload.
- wifi.py: two ad hoc, inconsistent bool coercions. POST
/wifi/ap/auto-enable used bare bool(), so a JSON string "false" enabled
it. POST /wifi/radio's enabled/force parsing recognized real bool and
some strings but not int 1/0 (1 is True is False in Python). Factored one
small _parse_bool_ish helper local to this file and used it at all three
sites.
Not changed: the "unknown/misspelled restore option keys default to True"
half of the backup.py finding -- the file's own comment documents that a
missing key deliberately means "restore everything," matching the
already-existing JSON-parse-failure guard a few lines above it; only the
bool-coercion defect was a real bug.
Added or extended regression tests for every fix, following each area's
existing test conventions. Full suite: 4328 passed, 62 skipped, 2 failed
on both this branch and origin/main (missing tzdata package breaks two
timezone-alias tests in test_onboarding_checklist.py, unrelated to this
change) -- no new failures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3bPMESe2TfrGvbs1ef9c5