mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
fix: address 6 code-review findings
NixOS module (4 fixes): - Remove RestrictNamespaces=true: Chromium (launched by single-file for web captures) needs Linux user namespaces; blocking them broke captures - Escape TOML string values: labels/paths with quotes or backslashes would produce invalid archivr-server.toml and prevent startup - Bracket IPv6 listen addresses: ::1:8080 is rejected by Rust's SocketAddr parser; [::1]:8080 is the correct form (RFC 2732) - Add optional storePath per archive: covers archives initialised with a custom store path outside the default sibling store/ directory Rust (1 fix): - extract_client_ip loopback branch: take last XFF value not first. When a proxy appends to X-Forwarded-For an attacker controls the first entry; the last entry is always set by the trusted local proxy Frontend (1 fix): - Gate fetchArchives on authState === 'authenticated'. The empty-dep useEffect fired on mount (before login), hit 401, and never retried after login because authState was not in the dependency array
This commit is contained in:
parent
71e176cbd1
commit
9544e707ed
3 changed files with 35 additions and 11 deletions
|
|
@ -174,13 +174,15 @@ fn extract_client_ip(req: &Request) -> IpAddr {
|
||||||
|
|
||||||
match peer_ip {
|
match peer_ip {
|
||||||
// Peer is a loopback address → the connection came from a local
|
// Peer is a loopback address → the connection came from a local
|
||||||
// reverse proxy (nginx/caddy on the same host). Trust the first
|
// reverse proxy (nginx/caddy on the same host). Trust the last
|
||||||
// address in X-Forwarded-For as the real client IP.
|
// address in X-Forwarded-For as the real client IP — the last entry
|
||||||
|
// is always appended by the trusted proxy, even if the client sent a
|
||||||
|
// spoofed value earlier in the chain.
|
||||||
Some(peer) if peer.is_loopback() => req
|
Some(peer) if peer.is_loopback() => req
|
||||||
.headers()
|
.headers()
|
||||||
.get("x-forwarded-for")
|
.get("x-forwarded-for")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.and_then(|s| s.split(',').next())
|
.and_then(|s| s.split(',').last())
|
||||||
.and_then(|s| s.trim().parse().ok())
|
.and_then(|s| s.trim().parse().ok())
|
||||||
.unwrap_or(peer),
|
.unwrap_or(peer),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,10 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Mount: load archives, pick first, then parallel load
|
// Load archives once authenticated (re-runs when authState changes so
|
||||||
|
// it triggers correctly after first login or a session refresh).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authState !== 'authenticated') return
|
||||||
fetchArchives().then(list => {
|
fetchArchives().then(list => {
|
||||||
setArchives(list)
|
setArchives(list)
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
|
|
@ -79,7 +81,7 @@ export default function App() {
|
||||||
setArchiveId(first)
|
setArchiveId(first)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [authState])
|
||||||
|
|
||||||
// Archive change: parallel load entries + runs + tags
|
// Archive change: parallel load entries + runs + tags
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,16 @@
|
||||||
let
|
let
|
||||||
cfg = config.services.archivr-server;
|
cfg = config.services.archivr-server;
|
||||||
|
|
||||||
|
# Escape characters that would break TOML double-quoted strings.
|
||||||
|
escapeTOML = s: builtins.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] s;
|
||||||
|
|
||||||
# Derived bind string from separate address + port options.
|
# Derived bind string from separate address + port options.
|
||||||
bindStr = "${cfg.listenAddress}:${toString cfg.port}";
|
# IPv6 addresses contain ":" and must be wrapped in brackets per RFC 2732.
|
||||||
|
bindStr =
|
||||||
|
let hasColon = builtins.match ".*:.*" cfg.listenAddress != null;
|
||||||
|
in if hasColon
|
||||||
|
then "[${cfg.listenAddress}]:${toString cfg.port}"
|
||||||
|
else "${cfg.listenAddress}:${toString cfg.port}";
|
||||||
|
|
||||||
# Generate the TOML registry file from NixOS options.
|
# Generate the TOML registry file from NixOS options.
|
||||||
# The auth DB is pinned to the StateDirectory so it survives upgrades.
|
# The auth DB is pinned to the StateDirectory so it survives upgrades.
|
||||||
|
|
@ -17,9 +25,9 @@ let
|
||||||
|
|
||||||
${lib.concatMapStrings (a: ''
|
${lib.concatMapStrings (a: ''
|
||||||
[[archives]]
|
[[archives]]
|
||||||
id = "${a.id}"
|
id = "${escapeTOML a.id}"
|
||||||
label = "${a.label}"
|
label = "${escapeTOML a.label}"
|
||||||
archive_path = "${a.path}"
|
archive_path = "${escapeTOML a.path}"
|
||||||
|
|
||||||
'') cfg.archives}
|
'') cfg.archives}
|
||||||
'';
|
'';
|
||||||
|
|
@ -73,6 +81,16 @@ in
|
||||||
The parent directory (which also contains the {file}`store/` blob directory)
|
The parent directory (which also contains the {file}`store/` blob directory)
|
||||||
is whitelisted for read-write access under systemd hardening.'';
|
is whitelisted for read-write access under systemd hardening.'';
|
||||||
};
|
};
|
||||||
|
storePath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Absolute path to the store directory for this archive. Only needed
|
||||||
|
when the archive was initialised with a custom store path outside
|
||||||
|
the default sibling {file}`store/` location. When null (the default),
|
||||||
|
the parent of {option}`path` already covers the standard layout.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default = [];
|
default = [];
|
||||||
|
|
@ -154,10 +172,12 @@ in
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: builtins.dirOf a.path) cfg.archives);
|
ReadWritePaths =
|
||||||
|
[ "/var/lib/archivr-server" ]
|
||||||
|
++ (map (a: builtins.dirOf a.path) cfg.archives)
|
||||||
|
++ (lib.concatMap (a: lib.optional (a.storePath != null) a.storePath) cfg.archives);
|
||||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||||
LockPersonality = true;
|
LockPersonality = true;
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictSUIDSGID = true;
|
RestrictSUIDSGID = true;
|
||||||
RemoveIPC = true;
|
RemoveIPC = true;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue