1
Fork 0
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:
TheGeneralist 2026-06-29 21:30:45 +02:00
parent 71e176cbd1
commit 9544e707ed
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
3 changed files with 35 additions and 11 deletions

View file

@ -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),

View file

@ -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(() => {

View file

@ -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;
};