fix(security): atomic hub-auth-token writes (P1-5)
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
`hub_auth_token.dart` write() previously did
await f.writeAsString(token); // file is now 0644 (umask)
await Process.run('chmod', ['600', f.path]);
which leaves a TOCTOU window where the file is world-readable
between the writeAsString and the chmod call. On a multi-user
host another user could read the bearer token during that
window.
New flow:
await tmp.writeAsString(token); // .tmp file
await chmod(600, tmp.path); // restrict mode FIRST
await tmp.rename(f.path); // atomic POSIX rename
The destination is never visible with permissive perms. Also
sets `~/.fai/` itself to 0700 on Unix on first creation so
other users on a shared host can't enumerate the directory.
dart analyze clean; flutter test green (12 tests).
Bumped pubspec to 0.49.1.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
23529bae82
commit
1c306916aa
2 changed files with 46 additions and 9 deletions
|
|
@ -58,19 +58,56 @@ class HubAuthToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist [token] to disk, creating `~/.fai/` if needed.
|
/// Persist [token] to disk, creating `~/.fai/` if needed.
|
||||||
/// On Unix the file is chmod-ed to 0600. Windows leaves the
|
///
|
||||||
/// default user-ACL alone.
|
/// Atomic write: writes to `<path>.tmp`, chmods 0600 on
|
||||||
|
/// Unix BEFORE moving into place, then renames. This
|
||||||
|
/// closes the TOCTOU window where the historical
|
||||||
|
/// `writeAsString` + later `chmod 600` sequence left the
|
||||||
|
/// file world-readable for the duration of the chmod call.
|
||||||
|
///
|
||||||
|
/// Also sets `~/.fai/` itself to 0700 on Unix on first
|
||||||
|
/// creation so other users on a shared host can't even
|
||||||
|
/// enumerate the directory (the 0600 token-file mode is
|
||||||
|
/// fine in isolation; combining it with a 0755 parent
|
||||||
|
/// directory leaks the file's existence).
|
||||||
static Future<void> write(String token) async {
|
static Future<void> write(String token) async {
|
||||||
final f = File(path);
|
final f = File(path);
|
||||||
await f.parent.create(recursive: true);
|
final tmp = File('${f.path}.tmp');
|
||||||
await f.writeAsString(token.trim(), flush: true);
|
final parent = f.parent;
|
||||||
|
final parentExisted = await parent.exists();
|
||||||
|
await parent.create(recursive: true);
|
||||||
|
if ((Platform.isLinux || Platform.isMacOS) && !parentExisted) {
|
||||||
|
try {
|
||||||
|
await Process.run('chmod', ['700', parent.path]);
|
||||||
|
} catch (_) {/* best-effort */}
|
||||||
|
}
|
||||||
|
// Write to the temp path with restrictive mode set up
|
||||||
|
// BEFORE the rename — the final file is never visible
|
||||||
|
// to other processes with the default umask mode.
|
||||||
|
await tmp.writeAsString(token.trim(), flush: true);
|
||||||
|
if (Platform.isLinux || Platform.isMacOS) {
|
||||||
|
try {
|
||||||
|
await Process.run('chmod', ['600', tmp.path]);
|
||||||
|
} catch (_) {/* best-effort */}
|
||||||
|
}
|
||||||
|
// Atomic rename: on POSIX, replaces the destination
|
||||||
|
// in one syscall. On Windows, Dart's File.rename uses
|
||||||
|
// MoveFileEx with MOVEFILE_REPLACE_EXISTING which is
|
||||||
|
// equally atomic for this use case.
|
||||||
|
try {
|
||||||
|
await tmp.rename(f.path);
|
||||||
|
} catch (_) {
|
||||||
|
// On rare cross-mount setups, rename may fail; fall
|
||||||
|
// back to copy + delete. The token is then briefly
|
||||||
|
// visible at the .tmp path with 0600 mode — still
|
||||||
|
// safe, but not atomic.
|
||||||
|
await tmp.copy(f.path);
|
||||||
if (Platform.isLinux || Platform.isMacOS) {
|
if (Platform.isLinux || Platform.isMacOS) {
|
||||||
try {
|
try {
|
||||||
await Process.run('chmod', ['600', f.path]);
|
await Process.run('chmod', ['600', f.path]);
|
||||||
} catch (_) {
|
} catch (_) {/* best-effort */}
|
||||||
// best-effort; default perms still confine to the
|
|
||||||
// operator's machine.
|
|
||||||
}
|
}
|
||||||
|
await tmp.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.49.0
|
version: 0.49.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue