Commit f52abed
authored
fix: settings-mutating routes preserve masked secrets (C2 + H4 + M3) (#224)
Internal security audit (May 2026): three findings, one root cause.
PR #215 added `_strip_masked_values` + `_deep_merge_dict` on the
central `/api/web/settings` POST path so a round-trip POST of the
masked GET response did not overwrite the on-disk credential with
the literal sentinel. The audit then found two OTHER mutation routes
that bypassed that pipeline.
### C2 (CRITICAL) — POST /api/storage/config
`StorageBackendConfig.post` wholesale-set the per-backend dict:
storage['azure_keyvault'] = data.get('azure_keyvault', {})
When the UI POSTed back the masked GET, the sentinel `'********'`
rode inside that dict and the deep-merge propagated it to the leaf
— overwriting the on-disk credential. Same shape for AWS / Vault /
Infisical.
Fix: call `_strip_masked_values` BEFORE `atomic_update`, same as
the settings POST path.
### H4 (HIGH) — POST /api/notifications/config
`s['notifications'] = data` wholesale-replaced the subtree. SMTP
`smtp_password` and webhook URLs with embedded auth tokens returned
masked on GET were destroyed on the round-trip POST. Toggling a
non-secret notifications field (e.g. `enabled`) silently wiped the
SMTP password.
Fix: strip masked values and deep-merge against the existing
on-disk subtree inside the `settings_manager.update` mutator.
### M3 (MEDIUM) — `notifications` missing from `_DEEP_MERGE_SETTINGS_KEYS`
Even a future caller using `atomic_update({'notifications': ...})`
would shallow-merge and wipe siblings. Adding `notifications` to
the deep-merge registry closes the gap for any caller.
### Changes
- `modules/core/settings.py` — `_DEEP_MERGE_SETTINGS_KEYS` now
includes `notifications`. Comment expanded to point at the audit
reasoning.
- `modules/api/resources.py::StorageBackendConfig.post` — strip
masked values via `_strip_masked_values` before `atomic_update`.
- `modules/web/misc_routes.py::api_notifications_config` (POST) —
strip masked values, deep-merge against the existing on-disk
notifications subtree inside the mutator.
### Tests
`tests/test_audit_settings_mutating_routes.py` (new, 9 cases):
- Registry: `notifications` is in `_DEEP_MERGE_SETTINGS_KEYS`;
`certificate_storage` + `ca_providers` still present (defensive
pin so a future refactor cannot silently drop them).
- StorageBackendConfig (HTTP integration test):
- Round-trip with masked `client_secret` preserves on-disk value.
- Genuine rotation (real new value) overwrites.
- Sibling backend's secret survives a single-backend update.
- Notifications POST (handler-logic level — avoids the state
pollution that registering the full misc_routes blueprint
causes when paired with the CertificateManager lock tests):
- Round-trip with masked `smtp_password` preserves on-disk.
- Genuine rotation overwrites.
- Empty-string secret preserves (same #215 semantic).
- Webhook URL list-merge contract pinned (list overlay
replaces wholesale — current behaviour; documented for any
future list-merge improvement).
Full local unit suite: 901 passed, 2 skipped, 115 deselected.1 parent 23fc9e4 commit f52abed
4 files changed
Lines changed: 361 additions & 15 deletions
File tree
- modules
- api
- core
- web
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1896 | 1896 | | |
1897 | 1897 | | |
1898 | 1898 | | |
1899 | | - | |
1900 | | - | |
1901 | | - | |
1902 | | - | |
1903 | | - | |
1904 | | - | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
| 1906 | + | |
| 1907 | + | |
| 1908 | + | |
| 1909 | + | |
| 1910 | + | |
| 1911 | + | |
| 1912 | + | |
1905 | 1913 | | |
1906 | | - | |
| 1914 | + | |
1907 | 1915 | | |
1908 | | - | |
| 1916 | + | |
1909 | 1917 | | |
1910 | | - | |
| 1918 | + | |
1911 | 1919 | | |
1912 | | - | |
| 1920 | + | |
1913 | 1921 | | |
1914 | | - | |
| 1922 | + | |
1915 | 1923 | | |
1916 | | - | |
| 1924 | + | |
| 1925 | + | |
1917 | 1926 | | |
1918 | 1927 | | |
1919 | 1928 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
227 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
228 | 238 | | |
229 | 239 | | |
230 | 240 | | |
| 241 | + | |
231 | 242 | | |
232 | 243 | | |
233 | 244 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
187 | 200 | | |
188 | | - | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
189 | 206 | | |
190 | 207 | | |
191 | 208 | | |
| |||
0 commit comments