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 {
|
||||
// Peer is a loopback address → the connection came from a local
|
||||
// reverse proxy (nginx/caddy on the same host). Trust the first
|
||||
// address in X-Forwarded-For as the real client IP.
|
||||
// reverse proxy (nginx/caddy on the same host). Trust the last
|
||||
// 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
|
||||
.headers()
|
||||
.get("x-forwarded-for")
|
||||
.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())
|
||||
.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(() => {
|
||||
if (authState !== 'authenticated') return
|
||||
fetchArchives().then(list => {
|
||||
setArchives(list)
|
||||
if (list.length > 0) {
|
||||
|
|
@ -79,7 +81,7 @@ export default function App() {
|
|||
setArchiveId(first)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
}, [authState])
|
||||
|
||||
// Archive change: parallel load entries + runs + tags
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,16 @@
|
|||
let
|
||||
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.
|
||||
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.
|
||||
# The auth DB is pinned to the StateDirectory so it survives upgrades.
|
||||
|
|
@ -17,9 +25,9 @@ let
|
|||
|
||||
${lib.concatMapStrings (a: ''
|
||||
[[archives]]
|
||||
id = "${a.id}"
|
||||
label = "${a.label}"
|
||||
archive_path = "${a.path}"
|
||||
id = "${escapeTOML a.id}"
|
||||
label = "${escapeTOML a.label}"
|
||||
archive_path = "${escapeTOML a.path}"
|
||||
|
||||
'') cfg.archives}
|
||||
'';
|
||||
|
|
@ -73,6 +81,16 @@ in
|
|||
The parent directory (which also contains the {file}`store/` blob directory)
|
||||
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 = [];
|
||||
|
|
@ -154,10 +172,12 @@ in
|
|||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
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" ];
|
||||
LockPersonality = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RemoveIPC = true;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue