srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include/wireframe/privileges.hpp
blob: 69df725778505d86231943c48ad7d439c578c796 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#pragma once

#ifndef _WIN32
#include <grp.h>
#include <unistd.h>
#endif

#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <string>

// After pcap_open_live() succeeds, the process has gotten everything
// CAP_NET_RAW exists for - running the rest of the program (decoding
// untrusted packet bytes, an interactive TUI/GUI event loop) as root
// from that point on is unnecessary exposure, and PLAN.md says as much
// directly: "Drop privileges immediately after opening the capture
// handle; use CAP_NET_RAW via file capabilities instead of running as
// root."
//
// The recommended path doesn't need this file at all: run
// `sudo setcap cap_net_raw+ep <binary>` once, then invoke the binary
// directly, unprivileged, forever after - CAP_NET_RAW alone is enough
// for pcap_open_live(), no root required at any point. This exists for
// the case someone still runs the binary via sudo (out of habit, or
// because setcap isn't available/permitted in some environments): drop
// straight back to the invoking user immediately, so the rest of the
// process's lifetime - including any -w output file, which then ends
// up owned by that user instead of root - runs unprivileged either way.
namespace wireframe {

// Drops from root to the user who actually invoked the program, via
// sudo's SUDO_UID/SUDO_GID (which sudo always sets). A no-op if not
// currently root, or if SUDO_UID isn't set (e.g. a genuine root login,
// not sudo - there's no "real" user to drop to in that case).
//
// setuid() to a nonzero UID also clears the process's Linux capability
// sets as a kernel-level side effect, so this covers both "running as
// root via sudo" and "root's own CAP_NET_RAW" the same way, without a
// separate libcap dependency.
//
// Returns an error message on failure. The drop is safety-critical: a
// failure here should be treated as fatal by the caller, not silently
// ignored while the process keeps running as root.
inline std::optional<std::string> drop_privileges_if_root() {
#ifdef _WIN32
    return std::nullopt;  // no POSIX privilege model to drop from
#else
    if (geteuid() != 0) return std::nullopt;  // already unprivileged

    const char* sudo_uid = std::getenv("SUDO_UID");
    const char* sudo_gid = std::getenv("SUDO_GID");
    if (sudo_uid == nullptr || sudo_gid == nullptr) {
        return std::nullopt;  // no safe target to drop to
    }

    uid_t target_uid = static_cast<uid_t>(std::strtoul(sudo_uid, nullptr, 10));
    gid_t target_gid = static_cast<gid_t>(std::strtoul(sudo_gid, nullptr, 10));

    // Order matters: groups and GID need root to change, so they must
    // be dropped before UID - once UID is gone, so is the privilege
    // to change the others.
    if (setgroups(1, &target_gid) == -1) {
        return "setgroups failed: " + std::string(std::strerror(errno));
    }
    if (setgid(target_gid) == -1) {
        return "setgid failed: " + std::string(std::strerror(errno));
    }
    if (setuid(target_uid) == -1) {
        return "setuid failed: " + std::string(std::strerror(errno));
    }

    // Defense in depth (standard advice from setuid-privilege-drop
    // write-ups): confirm root can't be reclaimed. If the saved-UID
    // was somehow left at 0, this would succeed and silently undo the
    // drop - so a *successful* setuid(0) here means something is
    // wrong, and is treated as the failure case.
    if (setuid(0) != -1) {
        return "failed to permanently drop root (setuid(0) unexpectedly succeeded)";
    }

    return std::nullopt;
#endif
}

}  // namespace wireframe