mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-21 02:19:07 +00:00
fix(web): stop /config/secrets handing out every credential it holds
GET /api/v3/config/secrets returned config_secrets.json in full to anyone who could reach the port, and this interface has no authentication. Probed against a real rig it produced six populated credential fields: a 40-character GitHub token, a 183-character Home Assistant token, and Jellyfin and weather API keys. This is the second door onto the same credentials; #477 closes the first. Masking the response alone would have been worse than the leak. The only client fetches every secret, edits one field and posts all of them back, and save_raw_file_content replaces the file wholesale -- so a masked GET followed by the client's own save would write the mask over every credential the user had not touched. That is why this was left open when the leak was found; it needs both halves. Read side: mask_all_secret_values(), which already existed for exactly this endpoint -- its docstring names it -- and had never been wired to a call site. It leaves empty values and YOUR_* placeholders alone, so a client can still tell "set" from "not set" without being told the secret. Write side: strip the echoed mask and blanks from the submission, then merge onto what is stored, so "unchanged" means unchanged. The cost is that a secret can no longer be cleared by blanking it; that wants its own affordance, since a control that erases credentials as a side effect of saving an unrelated one is not one. Browser side: the token field is now left empty rather than filled from the response. Filling it with the mask would have stored eight bullet characters as the token the next time the user pressed Save, and filling it with the real value is the thing being fixed. It reports whether a token is saved instead. Verified end to end through the Flask endpoints, not the helpers. Reverting the masking fails the leak tests; reverting the merge fails the preservation tests; both halves are independently guarded. 278 web tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
This commit is contained in:
co-authored by
Claude Opus 5
parent
cf0a551f7b
commit
d2379a1eb6
@@ -4622,15 +4622,17 @@ window.loadGithubToken = function() {
|
||||
// Handle empty data (secrets file doesn't exist) - API returns {} in this case
|
||||
const secrets = data.data || {};
|
||||
const token = secrets.github?.api_token || '';
|
||||
const configured = token && token !== 'YOUR_GITHUB_PERSONAL_ACCESS_TOKEN';
|
||||
|
||||
if (input) {
|
||||
if (token && token !== 'YOUR_GITHUB_PERSONAL_ACCESS_TOKEN') {
|
||||
// Token exists and is valid
|
||||
input.value = token;
|
||||
showNotification('GitHub token loaded successfully', 'success');
|
||||
// The endpoint masks what it returns, so this never holds
|
||||
// the real token -- and the field is deliberately left
|
||||
// empty rather than filled with the mask, which would be
|
||||
// saved verbatim the next time the user pressed Save.
|
||||
input.value = '';
|
||||
if (configured) {
|
||||
showNotification('A GitHub token is saved. Enter a new one to replace it.', 'success');
|
||||
} else {
|
||||
// No token configured or placeholder value
|
||||
input.value = '';
|
||||
showNotification('No GitHub token configured. Enter a new token to save.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user