security: remove setuid root from privileged helpers
This commit is contained in:
parent
420da2ebb4
commit
e99e386f4e
27 changed files with 994 additions and 645 deletions
|
|
@ -542,8 +542,8 @@ target_include_directories(BastionGuard
|
|||
)
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1111,29 +1111,9 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
install(CODE "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
# Security: never install this helper setuid. Privileged execution is requested
|
||||
# explicitly by PrivacyPage through pkexec.
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1157,29 +1137,9 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
install(CODE "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
# Security: never install this helper setuid. Backend invokes it through pkexec
|
||||
# and supplies a validated, per-request temporary input file.
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Maintainer: BastionGuard <info@bastionguard.eu>
|
||||
|
||||
pkgname=bastionguard
|
||||
pkgver=2.0.1
|
||||
pkgver=2.0.2
|
||||
pkgrel=0
|
||||
pkgdesc="Transparent security control plane for Linux desktops"
|
||||
url="https://bastionguard.eu/"
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
version=2.0.1
|
||||
build=20260726
|
||||
version=2.0.2
|
||||
build=20260727
|
||||
|
|
|
|||
13
debian/bastionguard.postinst
vendored
13
debian/bastionguard.postinst
vendored
|
|
@ -11,20 +11,7 @@ set -e
|
|||
case "$1" in
|
||||
configure|abort-upgrade|abort-deconfigure|abort-remove)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 1) setuid helpers via dpkg-statoverride (preferred)
|
||||
# ------------------------------------------------------------
|
||||
if command -v dpkg-statoverride >/dev/null 2>&1; then
|
||||
if [ -e /usr/bin/bastionguard-privhelper ]; then
|
||||
dpkg-statoverride --list /usr/bin/bastionguard-privhelper >/dev/null 2>&1 \
|
||||
|| dpkg-statoverride --add root root 4755 /usr/bin/bastionguard-privhelper || true
|
||||
fi
|
||||
|
||||
if [ -e /usr/bin/bastionguard-firewall ]; then
|
||||
dpkg-statoverride --list /usr/bin/bastionguard-firewall >/dev/null 2>&1 \
|
||||
|| dpkg-statoverride --add root root 4755 /usr/bin/bastionguard-firewall || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 2) CEF sandbox permissions (if installed)
|
||||
|
|
|
|||
50
debian/change_cmake/CMakeLists.txt
vendored
50
debian/change_cmake/CMakeLists.txt
vendored
|
|
@ -672,8 +672,8 @@ target_include_directories(BastionGuard
|
|||
)
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1201,29 +1201,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1247,29 +1224,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
2
debian/changelog
vendored
2
debian/changelog
vendored
|
|
@ -1,4 +1,4 @@
|
|||
bastionguard (2.0.1-1) stable; urgency=low
|
||||
bastionguard (2.0.2-2) stable; urgency=low
|
||||
|
||||
* Debian package.
|
||||
|
||||
|
|
|
|||
2
debian/changelog_ubuntu24
vendored
2
debian/changelog_ubuntu24
vendored
|
|
@ -1,4 +1,4 @@
|
|||
bastionguard (2.0.1-1ubuntu24.04.1) noble; urgency=low
|
||||
bastionguard (2.0.2-1ubuntu24.04.1) noble; urgency=low
|
||||
|
||||
* Ubuntu 24.04 (Noble) package.
|
||||
|
||||
|
|
|
|||
2
debian/changelog_ubuntu25
vendored
2
debian/changelog_ubuntu25
vendored
|
|
@ -1,4 +1,4 @@
|
|||
bastionguard (2.0.1-1ubuntu25.10) questing; urgency=low
|
||||
bastionguard (2.0.2-1ubuntu25.10) questing; urgency=low
|
||||
|
||||
* Ubuntu 25.10 (Questing Quokka) package.
|
||||
|
||||
|
|
|
|||
2
debian/changelog_ubuntu26
vendored
2
debian/changelog_ubuntu26
vendored
|
|
@ -1,4 +1,4 @@
|
|||
bastionguard (2.0.1-1ubuntu26.04) questing; urgency=low
|
||||
bastionguard (2.0.2-1ubuntu26.04) questing; urgency=low
|
||||
|
||||
* Ubuntu 26.04 (Resolute Raccoon) package.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
#
|
||||
|
||||
Name: bastionguard
|
||||
Version: 2.0.1
|
||||
Version: 2.0.2
|
||||
Release: 1%{?dist}
|
||||
Summary: BastionGuard Security Platform
|
||||
|
||||
|
|
|
|||
|
|
@ -736,8 +736,8 @@ target_include_directories(BastionGuard
|
|||
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1265,29 +1265,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1311,29 +1288,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
#
|
||||
|
||||
Name: bastionguard
|
||||
Version: 2.0.1
|
||||
Version: 2.0.2
|
||||
Release: %mkrel 1
|
||||
%global yara_version 4.5.5
|
||||
%global yara_stage %{_builddir}/%{name}-%{version}/.yara-stage
|
||||
|
|
|
|||
|
|
@ -757,8 +757,8 @@ target_include_directories(BastionGuard
|
|||
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1286,29 +1286,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1332,29 +1309,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
|
||||
Name: bastionguard
|
||||
Version: 2.0.1
|
||||
Version: 2.0.2
|
||||
Release: 1
|
||||
|
||||
%global yara_version 4.5.5
|
||||
|
|
|
|||
|
|
@ -763,8 +763,8 @@ target_include_directories(BastionGuard
|
|||
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1292,29 +1292,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1338,29 +1315,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
#
|
||||
|
||||
Name: bastionguard
|
||||
Version: 2.0.1
|
||||
Version: 2.0.2
|
||||
Release: 1leap
|
||||
Summary: BastionGuard Security Platform
|
||||
License: GPLv3
|
||||
|
|
|
|||
|
|
@ -719,8 +719,8 @@ target_include_directories(BastionGuard
|
|||
)
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1248,29 +1248,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1294,29 +1271,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
#
|
||||
|
||||
Name: bastionguard
|
||||
Version: 2.0.1
|
||||
Version: 2.0.2
|
||||
Release: 1
|
||||
Summary: BastionGuard Security Platform
|
||||
License: GPLv3
|
||||
|
|
|
|||
|
|
@ -719,8 +719,8 @@ target_include_directories(BastionGuard
|
|||
)
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1248,29 +1248,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1294,29 +1271,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
# Maintainer: BastionGuard info@bastionguard.eu
|
||||
|
||||
pkgname=bastionguard
|
||||
pkgver=2.0.1
|
||||
pkgver=2.0.2
|
||||
pkgrel=1
|
||||
pkgdesc="BastionGuard - transparent security control plane for Linux desktops"
|
||||
arch=('x86_64')
|
||||
|
|
|
|||
|
|
@ -636,8 +636,8 @@ target_include_directories(BastionGuard
|
|||
)
|
||||
|
||||
target_compile_definitions(BastionGuard PRIVATE
|
||||
BASTIONGUARD_VERSION="2.0.1"
|
||||
BASTIONGUARD_BUILD=20260726
|
||||
BASTIONGUARD_VERSION="2.0.2"
|
||||
BASTIONGUARD_BUILD=20260727
|
||||
)
|
||||
|
||||
target_link_libraries(BastionGuard
|
||||
|
|
@ -1189,29 +1189,6 @@ install(TARGETS bastionguard-privhelper
|
|||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
|
||||
")
|
||||
|
||||
# ======================
|
||||
# Helper privilegiato — bastionguard-firewall
|
||||
|
|
@ -1235,29 +1212,6 @@ install(TARGETS bastionguard-firewall
|
|||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
|
||||
bg_install_code( "
|
||||
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
|
||||
|
||||
execute_process(
|
||||
COMMAND chown root:root /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHOWN_RES
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
|
||||
RESULT_VARIABLE CHMOD_RES
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RES EQUAL 0)
|
||||
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
|
||||
endif()
|
||||
|
||||
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
|
||||
")
|
||||
|
||||
|
||||
# ======================
|
||||
|
|
|
|||
129
src/Backend.cpp
129
src/Backend.cpp
|
|
@ -35,6 +35,9 @@
|
|||
#include <stdexcept>
|
||||
#include <fstream>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
#include <regex>
|
||||
|
|
@ -2975,22 +2978,110 @@ Backend::ScanStatus Backend::get_current_scan_files()
|
|||
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
bool create_secure_firewall_input(
|
||||
const std::vector<std::string>& blacklistLines,
|
||||
std::string& outPath
|
||||
) {
|
||||
std::string pathTemplate = "/tmp/bastionguard-firewall-ips-XXXXXX";
|
||||
std::vector<char> pathBuffer(pathTemplate.begin(), pathTemplate.end());
|
||||
pathBuffer.push_back('\0');
|
||||
|
||||
const int fd = ::mkstemp(pathBuffer.data());
|
||||
if (fd < 0) {
|
||||
std::cerr << _("[Firewall] Impossibile creare il file temporaneo sicuro\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
outPath.assign(pathBuffer.data());
|
||||
|
||||
if (::fchmod(fd, S_IRUSR | S_IWUSR) != 0) {
|
||||
::close(fd);
|
||||
::unlink(outPath.c_str());
|
||||
outPath.clear();
|
||||
std::cerr << _("[Firewall] Impossibile proteggere il file temporaneo\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
::close(fd);
|
||||
|
||||
if (!BlacklistIpExtractor::extractAndWrite(blacklistLines, outPath)) {
|
||||
::unlink(outPath.c_str());
|
||||
outPath.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// extractAndWrite() riapre il file: imponiamo nuovamente 0600 e
|
||||
// verifichiamo che il pathname indichi ancora un singolo file regolare
|
||||
// appartenente all'utente che ha avviato BastionGuard.
|
||||
if (::chmod(outPath.c_str(), S_IRUSR | S_IWUSR) != 0) {
|
||||
::unlink(outPath.c_str());
|
||||
outPath.clear();
|
||||
std::cerr << _("[Firewall] Impossibile impostare i permessi del file temporaneo\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat st {};
|
||||
if (::lstat(outPath.c_str(), &st) != 0 ||
|
||||
!S_ISREG(st.st_mode) ||
|
||||
st.st_uid != ::getuid() ||
|
||||
st.st_nlink != 1 ||
|
||||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
|
||||
::unlink(outPath.c_str());
|
||||
outPath.clear();
|
||||
std::cerr << _("[Firewall] File temporaneo non sicuro\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int run_firewall_helper(const std::string& inputPath) {
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0)
|
||||
return -1;
|
||||
|
||||
if (pid == 0) {
|
||||
::execl(
|
||||
"/usr/bin/pkexec",
|
||||
"pkexec",
|
||||
"/usr/bin/bastionguard-firewall",
|
||||
"apply",
|
||||
inputPath.c_str(),
|
||||
static_cast<char*>(nullptr)
|
||||
);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (::waitpid(pid, &status, 0) < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!WIFEXITED(status))
|
||||
return -1;
|
||||
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Backend::applyFirewallFromBlacklist(
|
||||
const std::vector<std::string>& blacklistLines
|
||||
) {
|
||||
if (!firewallEnabled_ || firewallType_ == FirewallType::NONE)
|
||||
return;
|
||||
|
||||
const std::string outPath =
|
||||
"/tmp/bastionguard-firewall-ips.txt";
|
||||
|
||||
if (!BlacklistIpExtractor::extractAndWrite(
|
||||
blacklistLines, outPath)) {
|
||||
std::string outPath;
|
||||
if (!create_secure_firewall_input(blacklistLines, outPath)) {
|
||||
std::cout << _("[Firewall] Nessun IP valido estratto\n");
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleFirewallApply();
|
||||
scheduleFirewallApply(outPath);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -3082,29 +3173,35 @@ void Backend::applyFirewallFromPhishingBlacklist()
|
|||
fin.close();
|
||||
|
||||
|
||||
const std::string outPath =
|
||||
"/tmp/bastionguard-firewall-ips.txt";
|
||||
|
||||
if (!BlacklistIpExtractor::extractAndWrite(lines, outPath)) {
|
||||
std::string outPath;
|
||||
if (!create_secure_firewall_input(lines, outPath)) {
|
||||
std::cout << _("[Firewall] Nessun IP valido trovato\n");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
scheduleFirewallApply();
|
||||
scheduleFirewallApply(outPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Backend::scheduleFirewallApply() {
|
||||
void Backend::scheduleFirewallApply(const std::string& inputPath) {
|
||||
static std::atomic<bool> pending{false};
|
||||
|
||||
if (pending.exchange(true))
|
||||
if (pending.exchange(true)) {
|
||||
::unlink(inputPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread([] {
|
||||
std::thread([inputPath] {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
system("pkexec /usr/bin/bastionguard-firewall apply");
|
||||
|
||||
const int exitCode = run_firewall_helper(inputPath);
|
||||
if (exitCode != 0) {
|
||||
std::cerr << _("[Firewall] Applicazione helper fallita, codice: ")
|
||||
<< exitCode << "\n";
|
||||
::unlink(inputPath.c_str());
|
||||
}
|
||||
|
||||
pending.store(false);
|
||||
}).detach();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ public:
|
|||
void loadFirewallConfig();
|
||||
void saveFirewallConfig() const;
|
||||
void applyFirewallFromPhishingBlacklist();
|
||||
void scheduleFirewallApply();
|
||||
void scheduleFirewallApply(const std::string& inputPath);
|
||||
private:
|
||||
Backend();
|
||||
Backend(const Backend&) = delete;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <sys/wait.h>
|
||||
#include <glibmm/i18n.h>
|
||||
#include <glibmm/miscutils.h>
|
||||
#include <glibmm/main.h>
|
||||
|
|
@ -41,6 +43,36 @@ std::queue<std::string> alert_queue_;
|
|||
Gtk::Window* current_alert_ = nullptr;
|
||||
|
||||
|
||||
static bool run_privhelper_pkexec(const std::string& action) {
|
||||
if (action != "block" && action != "unblock")
|
||||
return false;
|
||||
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0)
|
||||
return false;
|
||||
|
||||
if (pid == 0) {
|
||||
::execl(
|
||||
"/usr/bin/pkexec",
|
||||
"pkexec",
|
||||
"/usr/bin/bastionguard-privhelper",
|
||||
action.c_str(),
|
||||
static_cast<char*>(nullptr)
|
||||
);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (::waitpid(pid, &status, 0) < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
|
||||
static std::string read_text_file(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
|
|
@ -313,9 +345,7 @@ void PrivacyPage::start_privacy_daemon() {
|
|||
|
||||
|
||||
void PrivacyPage::unblock_devices_local() {
|
||||
std::string cmd = "/usr/bin/bastionguard-privhelper unblock";
|
||||
int rc = std::system(cmd.c_str());
|
||||
if (WIFEXITED(rc) && WEXITSTATUS(rc) == 0)
|
||||
if (run_privhelper_pkexec("unblock"))
|
||||
write_log(_("📷 Webcam/Microfono sbloccati automaticamente"));
|
||||
else
|
||||
write_log(_("❌ Errore nello sblocco automatico"));
|
||||
|
|
@ -606,11 +636,7 @@ void PrivacyPage::show_next_alert() {
|
|||
|
||||
auto alive = alive_;
|
||||
std::thread([this, alive]() {
|
||||
std::string cmd = "/usr/bin/bastionguard-privhelper block";
|
||||
|
||||
int rc = std::system(cmd.c_str());
|
||||
|
||||
bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
|
||||
const bool ok = run_privhelper_pkexec("block");
|
||||
|
||||
Glib::signal_idle().connect_once([this, alive, ok]() {
|
||||
BastionGuard::if_alive(alive, [this, ok]() {
|
||||
|
|
@ -859,13 +885,7 @@ void PrivacyPage::run_privacy_script_async(bool unblock) {
|
|||
auto alive = alive_;
|
||||
std::thread([this, alive, unblock]() {
|
||||
|
||||
std::string cmd = "/usr/bin/bastionguard-privhelper ";
|
||||
cmd += (unblock ? "unblock" : "block");
|
||||
|
||||
|
||||
|
||||
int rc = std::system(cmd.c_str());
|
||||
const bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
|
||||
const bool ok = run_privhelper_pkexec(unblock ? "unblock" : "block");
|
||||
|
||||
|
||||
Glib::signal_idle().connect_once([this, alive, ok, unblock]() {
|
||||
|
|
@ -908,9 +928,7 @@ void PrivacyPage::run_privacy_script_async(bool unblock) {
|
|||
void PrivacyPage::manual_unlock_devices() {
|
||||
auto alive = alive_;
|
||||
std::thread([this, alive]() {
|
||||
std::string cmd = "/usr/bin/bastionguard-privhelper unblock";
|
||||
int rc = std::system(cmd.c_str());
|
||||
bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
|
||||
const bool ok = run_privhelper_pkexec("unblock");
|
||||
|
||||
Glib::signal_idle().connect_once([this, alive, ok]() {
|
||||
BastionGuard::if_alive(alive, [this, ok]() {
|
||||
|
|
|
|||
|
|
@ -18,149 +18,367 @@
|
|||
* The BastionGuard™ name and branding are not licensed under the GPL.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
static constexpr const char* BLACKLIST_PATH =
|
||||
"/tmp/bastionguard-firewall-ips.txt";
|
||||
namespace {
|
||||
|
||||
static bool run_cmd(const std::string& cmd) {
|
||||
int rc = std::system(cmd.c_str());
|
||||
return WIFEXITED(rc) && WEXITSTATUS(rc) == 0;
|
||||
}
|
||||
constexpr std::string_view kInputPrefix = "/tmp/bastionguard-firewall-ips-";
|
||||
constexpr off_t kMaxInputBytes = 16 * 1024 * 1024;
|
||||
constexpr std::size_t kMaxEntries = 250000;
|
||||
constexpr std::size_t kMaxLineLength = 128;
|
||||
|
||||
static bool command_exists(const std::string& cmd) {
|
||||
return run_cmd("command -v " + cmd + " >/dev/null 2>&1");
|
||||
}
|
||||
struct SecureInput {
|
||||
int fd{-1};
|
||||
std::string path;
|
||||
|
||||
static std::vector<std::string> load_blacklist() {
|
||||
std::vector<std::string> ips;
|
||||
SecureInput() = default;
|
||||
SecureInput(int input_fd, std::string input_path)
|
||||
: fd(input_fd), path(std::move(input_path)) {}
|
||||
|
||||
std::ifstream fin(BLACKLIST_PATH);
|
||||
if (!fin) {
|
||||
std::cerr << _("[Firewall] Impossibile aprire blacklist: ")
|
||||
<< BLACKLIST_PATH << "\n";
|
||||
return ips;
|
||||
SecureInput(const SecureInput&) = delete;
|
||||
SecureInput& operator=(const SecureInput&) = delete;
|
||||
|
||||
SecureInput(SecureInput&& other) noexcept
|
||||
: fd(other.fd), path(std::move(other.path)) {
|
||||
other.fd = -1;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(fin, line)) {
|
||||
if (line.empty()) continue;
|
||||
if (line[0] == '#') continue;
|
||||
ips.push_back(line);
|
||||
SecureInput& operator=(SecureInput&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (fd >= 0) ::close(fd);
|
||||
fd = other.fd;
|
||||
path = std::move(other.path);
|
||||
other.fd = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SecureInput() {
|
||||
if (fd >= 0) ::close(fd);
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<uid_t> parse_uid(const char* value) {
|
||||
if (!value || !*value) return std::nullopt;
|
||||
|
||||
unsigned long long parsed = 0;
|
||||
for (const unsigned char ch : std::string(value)) {
|
||||
if (!std::isdigit(ch)) return std::nullopt;
|
||||
parsed = parsed * 10 + static_cast<unsigned long long>(ch - '0');
|
||||
if (parsed > std::numeric_limits<uid_t>::max()) return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<uid_t>(parsed);
|
||||
}
|
||||
|
||||
uid_t original_caller_uid() {
|
||||
if (const auto uid = parse_uid(std::getenv("PKEXEC_UID"))) {
|
||||
return *uid;
|
||||
}
|
||||
return ::getuid();
|
||||
}
|
||||
|
||||
bool allowed_input_path(const std::string& path) {
|
||||
if (!path.starts_with(kInputPrefix)) return false;
|
||||
|
||||
const std::string_view suffix(path.data() + kInputPrefix.size(),
|
||||
path.size() - kInputPrefix.size());
|
||||
return !suffix.empty() && suffix.find('/') == std::string_view::npos;
|
||||
}
|
||||
|
||||
std::optional<SecureInput> open_input_securely(const std::string& path,
|
||||
uid_t caller_uid) {
|
||||
if (!allowed_input_path(path)) {
|
||||
std::cerr << _("[Firewall] Percorso input non autorizzato.\n");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
||||
if (fd < 0) {
|
||||
std::cerr << _("[Firewall] Impossibile aprire il file input: ")
|
||||
<< std::strerror(errno) << "\n";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
struct stat st {};
|
||||
if (::fstat(fd, &st) != 0) {
|
||||
std::cerr << _("[Firewall] fstat fallita: ")
|
||||
<< std::strerror(errno) << "\n";
|
||||
::close(fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const bool ownership_ok =
|
||||
(caller_uid == 0 && st.st_uid == 0) ||
|
||||
(caller_uid != 0 && st.st_uid == caller_uid);
|
||||
|
||||
if (!S_ISREG(st.st_mode) ||
|
||||
st.st_nlink != 1 ||
|
||||
!ownership_ok ||
|
||||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0 ||
|
||||
st.st_size <= 0 ||
|
||||
st.st_size > kMaxInputBytes) {
|
||||
std::cerr << _("[Firewall] File input non sicuro o non valido.\n");
|
||||
::close(fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The descriptor remains valid after unlink and cannot be replaced through
|
||||
// the pathname while the privileged process is reading it.
|
||||
if (::unlink(path.c_str()) != 0) {
|
||||
std::cerr << _("[Firewall] Impossibile rimuovere il file temporaneo: ")
|
||||
<< std::strerror(errno) << "\n";
|
||||
::close(fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return SecureInput(fd, path);
|
||||
}
|
||||
|
||||
std::string trim(std::string value) {
|
||||
const auto first = value.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return {};
|
||||
const auto last = value.find_last_not_of(" \t\r\n");
|
||||
return value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
bool is_valid_ip(const std::string& value) {
|
||||
if (value.empty() || value.size() > kMaxLineLength) return false;
|
||||
|
||||
in_addr ipv4 {};
|
||||
if (::inet_pton(AF_INET, value.c_str(), &ipv4) == 1) return true;
|
||||
|
||||
in6_addr ipv6 {};
|
||||
return ::inet_pton(AF_INET6, value.c_str(), &ipv6) == 1;
|
||||
}
|
||||
|
||||
std::vector<std::string> load_and_validate_ips(int fd) {
|
||||
std::string data;
|
||||
data.reserve(64 * 1024);
|
||||
|
||||
char buffer[8192];
|
||||
while (true) {
|
||||
const ssize_t count = ::read(fd, buffer, sizeof(buffer));
|
||||
if (count == 0) break;
|
||||
if (count < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
std::cerr << _("[Firewall] Errore lettura file input: ")
|
||||
<< std::strerror(errno) << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (data.size() + static_cast<std::size_t>(count) >
|
||||
static_cast<std::size_t>(kMaxInputBytes)) {
|
||||
std::cerr << _("[Firewall] File input troppo grande.\n");
|
||||
return {};
|
||||
}
|
||||
data.append(buffer, static_cast<std::size_t>(count));
|
||||
}
|
||||
|
||||
std::vector<std::string> ips;
|
||||
std::unordered_set<std::string> seen;
|
||||
std::size_t start = 0;
|
||||
|
||||
while (start <= data.size()) {
|
||||
const auto end = data.find('\n', start);
|
||||
std::string line = trim(data.substr(
|
||||
start,
|
||||
end == std::string::npos ? std::string::npos : end - start));
|
||||
|
||||
if (!line.empty() && line.front() != '#') {
|
||||
if (!is_valid_ip(line)) {
|
||||
std::cerr << _("[Firewall] Voce IP rifiutata: ") << line << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (seen.insert(line).second) {
|
||||
ips.push_back(std::move(line));
|
||||
if (ips.size() > kMaxEntries) {
|
||||
std::cerr << _("[Firewall] Troppe voci nel file input.\n");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return ips;
|
||||
}
|
||||
|
||||
static bool apply_ufw(const std::vector<std::string>& ips) {
|
||||
if (!command_exists("ufw"))
|
||||
std::optional<std::string> find_program(
|
||||
std::initializer_list<const char*> candidates) {
|
||||
for (const char* candidate : candidates) {
|
||||
if (::access(candidate, X_OK) == 0) return std::string(candidate);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool run_program(const std::string& executable,
|
||||
const std::vector<std::string>& arguments,
|
||||
bool silence_stderr = false) {
|
||||
std::vector<std::string> storage;
|
||||
storage.reserve(arguments.size() + 1);
|
||||
storage.push_back(executable);
|
||||
storage.insert(storage.end(), arguments.begin(), arguments.end());
|
||||
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(storage.size() + 1);
|
||||
for (auto& value : storage) argv.push_back(value.data());
|
||||
argv.push_back(nullptr);
|
||||
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return false;
|
||||
|
||||
if (pid == 0) {
|
||||
if (silence_stderr) {
|
||||
const int null_fd = ::open("/dev/null", O_WRONLY | O_CLOEXEC);
|
||||
if (null_fd >= 0) {
|
||||
::dup2(null_fd, STDERR_FILENO);
|
||||
if (null_fd != STDERR_FILENO) ::close(null_fd);
|
||||
}
|
||||
}
|
||||
|
||||
::execv(executable.c_str(), argv.data());
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (::waitpid(pid, &status, 0) < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
run_cmd("ufw --force enable");
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
bool apply_ufw(const std::vector<std::string>& ips) {
|
||||
const auto ufw = find_program({"/usr/sbin/ufw", "/usr/bin/ufw"});
|
||||
if (!ufw) return false;
|
||||
|
||||
if (!run_program(*ufw, {"--force", "enable"})) return false;
|
||||
|
||||
bool success = true;
|
||||
for (const auto& ip : ips) {
|
||||
run_cmd("ufw deny from " + ip);
|
||||
if (!run_program(*ufw, {"deny", "from", ip})) success = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool apply_firewalld(const std::vector<std::string>& ips) {
|
||||
if (!command_exists("firewall-cmd"))
|
||||
return false;
|
||||
bool apply_firewalld(const std::vector<std::string>& ips) {
|
||||
const auto firewall_cmd =
|
||||
find_program({"/usr/bin/firewall-cmd", "/usr/sbin/firewall-cmd"});
|
||||
if (!firewall_cmd) return false;
|
||||
|
||||
run_cmd(
|
||||
"firewall-cmd --permanent "
|
||||
"--new-ipset=bastionguard-blacklist "
|
||||
"--type=hash:ip 2>/dev/null"
|
||||
);
|
||||
|
||||
run_cmd(
|
||||
"firewall-cmd --permanent "
|
||||
"--zone=public --add-source-ipset=bastionguard-blacklist"
|
||||
);
|
||||
// These operations are idempotent from BastionGuard's perspective. An
|
||||
// already existing ipset/source association is not fatal.
|
||||
run_program(*firewall_cmd,
|
||||
{"--permanent", "--new-ipset=bastionguard-blacklist",
|
||||
"--type=hash:ip"},
|
||||
true);
|
||||
run_program(*firewall_cmd,
|
||||
{"--permanent", "--zone=public",
|
||||
"--add-source-ipset=bastionguard-blacklist"},
|
||||
true);
|
||||
|
||||
bool success = true;
|
||||
for (const auto& ip : ips) {
|
||||
run_cmd(
|
||||
"firewall-cmd --permanent "
|
||||
"--ipset=bastionguard-blacklist --add-entry=" + ip
|
||||
);
|
||||
if (!run_program(*firewall_cmd,
|
||||
{"--permanent",
|
||||
"--ipset=bastionguard-blacklist",
|
||||
"--add-entry=" + ip},
|
||||
true)) {
|
||||
// Duplicate entries may return a non-zero status. Continue and let
|
||||
// reload determine whether firewalld is operational.
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
run_cmd("firewall-cmd --reload");
|
||||
return true;
|
||||
const bool reloaded = run_program(*firewall_cmd, {"--reload"});
|
||||
return reloaded && (success || !ips.empty());
|
||||
}
|
||||
|
||||
static void clear_firewall() {
|
||||
if (command_exists("ufw")) {
|
||||
run_cmd("ufw --force reset");
|
||||
void clear_firewall() {
|
||||
if (const auto ufw = find_program({"/usr/sbin/ufw", "/usr/bin/ufw"})) {
|
||||
run_program(*ufw, {"--force", "reset"});
|
||||
}
|
||||
|
||||
if (command_exists("firewall-cmd")) {
|
||||
run_cmd(
|
||||
"firewall-cmd --permanent "
|
||||
"--delete-ipset=bastionguard-blacklist 2>/dev/null"
|
||||
);
|
||||
run_cmd("firewall-cmd --reload");
|
||||
if (const auto firewall_cmd =
|
||||
find_program({"/usr/bin/firewall-cmd", "/usr/sbin/firewall-cmd"})) {
|
||||
run_program(*firewall_cmd,
|
||||
{"--permanent", "--delete-ipset=bastionguard-blacklist"},
|
||||
true);
|
||||
run_program(*firewall_cmd, {"--reload"});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const uid_t caller_uid = original_caller_uid();
|
||||
|
||||
clearenv();
|
||||
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
|
||||
::clearenv();
|
||||
::setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
|
||||
|
||||
if (geteuid() != 0) {
|
||||
std::cerr << _("[Firewall] Deve essere eseguito come root.\n");
|
||||
if (::geteuid() != 0) {
|
||||
std::cerr << _("[Firewall] Deve essere eseguito come root tramite pkexec.\n");
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (argc != 2) {
|
||||
std::cerr << "Uso: " << argv[0] << " apply|clear\n";
|
||||
if (argc < 2) {
|
||||
std::cerr << "Uso: " << argv[0] << " apply <input-file>|clear\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string action = argv[1];
|
||||
const std::string action = argv[1];
|
||||
|
||||
if (action == "clear") {
|
||||
if (argc != 2) return 1;
|
||||
clear_firewall();
|
||||
fs::remove(BLACKLIST_PATH);
|
||||
std::cout << _("[Firewall] Regole rimosse.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (action != "apply") {
|
||||
std::cerr << _("[Firewall] Comando non valido: ") << action << "\n";
|
||||
if (action != "apply" || argc != 3) {
|
||||
std::cerr << _("[Firewall] Comando non valido.\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto ips = load_blacklist();
|
||||
auto input = open_input_securely(argv[2], caller_uid);
|
||||
if (!input) return 3;
|
||||
|
||||
auto ips = load_and_validate_ips(input->fd);
|
||||
if (ips.empty()) {
|
||||
std::cerr << _("[Firewall] Blacklist vuota o non valida.\n");
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
bool ok =
|
||||
apply_firewalld(ips) ||
|
||||
apply_ufw(ips);
|
||||
|
||||
fs::remove(BLACKLIST_PATH);
|
||||
const bool ok = apply_firewalld(ips) || apply_ufw(ips);
|
||||
|
||||
if (ok) {
|
||||
std::cout << _("[Firewall] Regole applicate (")
|
||||
<< ips.size() << " IP).\n";
|
||||
<< ips.size() << _(" IP).\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cerr << _("[Firewall] Nessun firewall supportato trovato.\n");
|
||||
std::cerr << _("[Firewall] Nessun firewall supportato o applicazione fallita.\n");
|
||||
return 5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,138 +18,540 @@
|
|||
* The BastionGuard™ name and branding are not licensed under the GPL.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <iostream>
|
||||
#include <pwd.h>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <array>
|
||||
#include <glib/gi18n.h>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
|
||||
constexpr const char* kStateDir = "/run/bastionguard";
|
||||
constexpr const char* kStateFile = "/run/bastionguard/camera-device-modes";
|
||||
constexpr std::size_t kMaxPipewireOutput = 1024U * 1024U;
|
||||
|
||||
static std::string exec_shell(const std::string& cmd) {
|
||||
std::array<char, 256> buffer;
|
||||
std::string result;
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) return {};
|
||||
while (fgets(buffer.data(), buffer.size(), pipe))
|
||||
result += buffer.data();
|
||||
pclose(pipe);
|
||||
return result;
|
||||
struct CallerIdentity {
|
||||
uid_t uid{0};
|
||||
gid_t gid{0};
|
||||
std::string name{"root"};
|
||||
std::string home{"/root"};
|
||||
};
|
||||
|
||||
bool parse_uid(const char* value, uid_t& out)
|
||||
{
|
||||
if (!value || !*value)
|
||||
return false;
|
||||
|
||||
errno = 0;
|
||||
char* end = nullptr;
|
||||
const unsigned long parsed = std::strtoul(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' || parsed > UINT_MAX)
|
||||
return false;
|
||||
|
||||
out = static_cast<uid_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool has_pipewire() {
|
||||
int rc = std::system("/usr/bin/pw-cli info 0 > /dev/null 2>&1");
|
||||
return (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
|
||||
}
|
||||
CallerIdentity resolve_caller_identity()
|
||||
{
|
||||
uid_t uid = getuid();
|
||||
|
||||
static void block_v4l2_devices() {
|
||||
for (const auto& e : fs::directory_iterator("/dev")) {
|
||||
auto p = e.path().string();
|
||||
if (p.rfind("/dev/video", 0) == 0) {
|
||||
chmod(p.c_str(), 0000);
|
||||
if (uid == 0) {
|
||||
uid_t env_uid = 0;
|
||||
if (parse_uid(std::getenv("PKEXEC_UID"), env_uid) ||
|
||||
parse_uid(std::getenv("SUDO_UID"), env_uid)) {
|
||||
uid = env_uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unblock_v4l2_devices() {
|
||||
for (const auto& e : fs::directory_iterator("/dev")) {
|
||||
auto p = e.path().string();
|
||||
if (p.rfind("/dev/video", 0) == 0) {
|
||||
chmod(p.c_str(), 0660);
|
||||
}
|
||||
CallerIdentity identity;
|
||||
identity.uid = uid;
|
||||
|
||||
if (passwd* pw = getpwuid(uid)) {
|
||||
identity.gid = pw->pw_gid;
|
||||
if (pw->pw_name)
|
||||
identity.name = pw->pw_name;
|
||||
if (pw->pw_dir && *pw->pw_dir)
|
||||
identity.home = pw->pw_dir;
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
static bool pipewire_disable_camera() {
|
||||
std::string list = exec_shell("/usr/bin/pw-cli ls Node");
|
||||
std::istringstream ss(list);
|
||||
std::string line;
|
||||
std::vector<std::string> ids;
|
||||
while (std::getline(ss, line)) {
|
||||
if (line.find("id ") != std::string::npos) {
|
||||
std::string id = line.substr(line.find("id ") + 3);
|
||||
id = id.substr(0, id.find_first_of(", \t\n"));
|
||||
std::streampos cur = ss.tellg();
|
||||
std::string sub;
|
||||
bool isVideoDevice = false;
|
||||
while (std::getline(ss, sub)) {
|
||||
if (sub.find("media.class") != std::string::npos &&
|
||||
sub.find("Video/Device") != std::string::npos) {
|
||||
isVideoDevice = true;
|
||||
break;
|
||||
}
|
||||
if (sub.rfind("id ", 0) == 0)
|
||||
break;
|
||||
bool runtime_dir_is_safe(uid_t uid, const std::string& path)
|
||||
{
|
||||
struct stat st {};
|
||||
if (lstat(path.c_str(), &st) != 0)
|
||||
return false;
|
||||
|
||||
return S_ISDIR(st.st_mode) &&
|
||||
st.st_uid == uid &&
|
||||
(st.st_mode & (S_IWGRP | S_IWOTH)) == 0;
|
||||
}
|
||||
|
||||
bool drop_to_caller(const CallerIdentity& caller)
|
||||
{
|
||||
if (caller.uid == 0)
|
||||
return true;
|
||||
|
||||
if (initgroups(caller.name.c_str(), caller.gid) != 0)
|
||||
return false;
|
||||
if (setresgid(caller.gid, caller.gid, caller.gid) != 0)
|
||||
return false;
|
||||
if (setresuid(caller.uid, caller.uid, caller.uid) != 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<char*> make_exec_argv(const std::vector<std::string>& args)
|
||||
{
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(args.size() + 1);
|
||||
for (const auto& arg : args)
|
||||
argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
argv.push_back(nullptr);
|
||||
return argv;
|
||||
}
|
||||
|
||||
bool run_as_caller(const CallerIdentity& caller,
|
||||
const std::vector<std::string>& args,
|
||||
std::string* output = nullptr)
|
||||
{
|
||||
if (args.empty() || args.front().empty() || args.front().front() != '/')
|
||||
return false;
|
||||
|
||||
int pipefd[2] = {-1, -1};
|
||||
if (output && pipe2(pipefd, O_CLOEXEC) != 0)
|
||||
return false;
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
if (output) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
if (output) {
|
||||
close(pipefd[0]);
|
||||
if (dup2(pipefd[1], STDOUT_FILENO) < 0)
|
||||
_exit(126);
|
||||
close(pipefd[1]);
|
||||
}
|
||||
|
||||
if (!drop_to_caller(caller))
|
||||
_exit(126);
|
||||
|
||||
clearenv();
|
||||
setenv("PATH", "/usr/bin:/bin", 1);
|
||||
setenv("HOME", caller.home.c_str(), 1);
|
||||
setenv("USER", caller.name.c_str(), 1);
|
||||
setenv("LOGNAME", caller.name.c_str(), 1);
|
||||
|
||||
const std::string runtime = "/run/user/" + std::to_string(caller.uid);
|
||||
if (runtime_dir_is_safe(caller.uid, runtime))
|
||||
setenv("XDG_RUNTIME_DIR", runtime.c_str(), 1);
|
||||
|
||||
auto argv = make_exec_argv(args);
|
||||
execv(args.front().c_str(), argv.data());
|
||||
_exit(errno == ENOENT ? 127 : 126);
|
||||
}
|
||||
|
||||
if (output) {
|
||||
close(pipefd[1]);
|
||||
output->clear();
|
||||
|
||||
char buffer[4096];
|
||||
while (output->size() < kMaxPipewireOutput) {
|
||||
const ssize_t n = read(pipefd[0], buffer, sizeof(buffer));
|
||||
if (n > 0) {
|
||||
const std::size_t remaining = kMaxPipewireOutput - output->size();
|
||||
output->append(buffer, std::min<std::size_t>(static_cast<std::size_t>(n), remaining));
|
||||
continue;
|
||||
}
|
||||
if (isVideoDevice)
|
||||
ids.push_back(id);
|
||||
ss.clear();
|
||||
ss.seekg(cur);
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
close(pipefd[0]);
|
||||
}
|
||||
bool success = true;
|
||||
for (auto &id : ids) {
|
||||
std::string cmd = "/usr/bin/pw-cli set-param " + id + " Device/Disabled true";
|
||||
int rc = std::system(cmd.c_str());
|
||||
if (!(WIFEXITED(rc) && WEXITSTATUS(rc) == 0)) {
|
||||
std::cerr << _("Errore disabilitando nodo PipeWire id=") << id << "\n";
|
||||
success = false;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (waitpid(pid, &status, 0) < 0) {
|
||||
if (errno != EINTR)
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
static bool pipewire_enable_camera() {
|
||||
std::string list = exec_shell("pw-cli ls Node");
|
||||
std::istringstream ss(list);
|
||||
std::string line;
|
||||
std::string find_pw_cli()
|
||||
{
|
||||
static constexpr const char* candidates[] = {
|
||||
"/usr/bin/pw-cli",
|
||||
"/bin/pw-cli"
|
||||
};
|
||||
|
||||
for (const char* candidate : candidates) {
|
||||
if (access(candidate, X_OK) == 0)
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool is_decimal_id(const std::string& value)
|
||||
{
|
||||
return !value.empty() &&
|
||||
value.size() <= 10 &&
|
||||
std::all_of(value.begin(), value.end(), [](unsigned char c) {
|
||||
return std::isdigit(c) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> parse_video_node_ids(const std::string& listing)
|
||||
{
|
||||
std::vector<std::string> ids;
|
||||
while (std::getline(ss, line)) {
|
||||
if (line.find("id ") != std::string::npos) {
|
||||
std::string id = line.substr(line.find("id ") + 3);
|
||||
id = id.substr(0, id.find_first_of(", \t\n"));
|
||||
std::streampos cur = ss.tellg();
|
||||
std::string sub;
|
||||
bool isVideoDevice = false;
|
||||
while (std::getline(ss, sub)) {
|
||||
if (sub.find("media.class") != std::string::npos &&
|
||||
sub.find("Video/Device") != std::string::npos) {
|
||||
isVideoDevice = true;
|
||||
break;
|
||||
}
|
||||
if (sub.rfind("id ", 0) == 0)
|
||||
break;
|
||||
}
|
||||
if (isVideoDevice)
|
||||
ids.push_back(id);
|
||||
ss.clear();
|
||||
ss.seekg(cur);
|
||||
std::set<std::string> seen;
|
||||
std::istringstream stream(listing);
|
||||
|
||||
std::string current_id;
|
||||
bool current_is_video = false;
|
||||
|
||||
auto commit = [&]() {
|
||||
if (current_is_video && is_decimal_id(current_id) && seen.insert(current_id).second)
|
||||
ids.push_back(current_id);
|
||||
current_id.clear();
|
||||
current_is_video = false;
|
||||
};
|
||||
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
const std::size_t id_pos = line.find("id ");
|
||||
if (id_pos != std::string::npos) {
|
||||
commit();
|
||||
|
||||
std::size_t begin = id_pos + 3;
|
||||
while (begin < line.size() && std::isspace(static_cast<unsigned char>(line[begin])))
|
||||
++begin;
|
||||
|
||||
std::size_t end = begin;
|
||||
while (end < line.size() && std::isdigit(static_cast<unsigned char>(line[end])))
|
||||
++end;
|
||||
|
||||
if (end > begin)
|
||||
current_id = line.substr(begin, end - begin);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!current_id.empty() &&
|
||||
line.find("media.class") != std::string::npos &&
|
||||
line.find("Video/Device") != std::string::npos) {
|
||||
current_is_video = true;
|
||||
}
|
||||
}
|
||||
bool success = true;
|
||||
for (auto &id : ids) {
|
||||
std::string cmd = "pw-cli set-param " + id + " Device/Disabled false";
|
||||
int rc = std::system(cmd.c_str());
|
||||
if (!(WIFEXITED(rc) && WEXITSTATUS(rc) == 0)) {
|
||||
std::cerr << _("Errore riabilitando nodo PipeWire id=") << id << "\n";
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
|
||||
commit();
|
||||
return ids;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
bool set_pipewire_camera_state(const CallerIdentity& caller, bool disabled)
|
||||
{
|
||||
const std::string pw_cli = find_pw_cli();
|
||||
if (pw_cli.empty())
|
||||
return false;
|
||||
|
||||
std::string listing;
|
||||
if (!run_as_caller(caller, {pw_cli, "ls", "Node"}, &listing))
|
||||
return false;
|
||||
|
||||
const auto ids = parse_video_node_ids(listing);
|
||||
if (ids.empty())
|
||||
return false;
|
||||
|
||||
bool all_ok = true;
|
||||
for (const auto& id : ids) {
|
||||
const bool ok = run_as_caller(
|
||||
caller,
|
||||
{pw_cli, "set-param", id, "Device/Disabled", disabled ? "true" : "false"}
|
||||
);
|
||||
if (!ok) {
|
||||
std::cerr << "[Privacy] Unable to update PipeWire camera node " << id << "\n";
|
||||
all_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
return all_ok;
|
||||
}
|
||||
|
||||
bool is_video_device_name(const std::string& name)
|
||||
{
|
||||
static constexpr const char prefix[] = "video";
|
||||
if (name.rfind(prefix, 0) != 0 || name.size() <= sizeof(prefix) - 1)
|
||||
return false;
|
||||
|
||||
return std::all_of(name.begin() + static_cast<std::ptrdiff_t>(sizeof(prefix) - 1),
|
||||
name.end(),
|
||||
[](unsigned char c) { return std::isdigit(c) != 0; });
|
||||
}
|
||||
|
||||
bool ensure_state_directory()
|
||||
{
|
||||
struct stat st {};
|
||||
if (lstat(kStateDir, &st) != 0) {
|
||||
if (errno != ENOENT || mkdir(kStateDir, 0755) != 0)
|
||||
return false;
|
||||
if (lstat(kStateDir, &st) != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!S_ISDIR(st.st_mode) || st.st_uid != 0 ||
|
||||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chown(kStateDir, 0, 0) != 0 || chmod(kStateDir, 0755) != 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int open_video_device(int dev_dir_fd, const std::string& name, struct stat& st)
|
||||
{
|
||||
if (!is_video_device_name(name))
|
||||
return -1;
|
||||
|
||||
const int fd = openat(dev_dir_fd,
|
||||
name.c_str(),
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
if (fstat(fd, &st) != 0 || !S_ISCHR(st.st_mode)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
bool block_v4l2_devices()
|
||||
{
|
||||
if (!ensure_state_directory()) {
|
||||
std::cerr << "[Privacy] Unsafe or unavailable state directory\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
const int state_fd = open(kStateFile,
|
||||
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW,
|
||||
0600);
|
||||
if (state_fd < 0)
|
||||
return false;
|
||||
|
||||
struct stat state_st {};
|
||||
if (fstat(state_fd, &state_st) != 0 ||
|
||||
!S_ISREG(state_st.st_mode) ||
|
||||
state_st.st_uid != 0 ||
|
||||
state_st.st_nlink != 1) {
|
||||
close(state_fd);
|
||||
unlink(kStateFile);
|
||||
return false;
|
||||
}
|
||||
fchmod(state_fd, 0600);
|
||||
|
||||
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dev_fd < 0) {
|
||||
close(state_fd);
|
||||
unlink(kStateFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
DIR* dev_dir = fdopendir(dev_fd);
|
||||
if (!dev_dir) {
|
||||
close(dev_fd);
|
||||
close(state_fd);
|
||||
unlink(kStateFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t blocked = 0;
|
||||
while (dirent* entry = readdir(dev_dir)) {
|
||||
const std::string name(entry->d_name);
|
||||
struct stat st {};
|
||||
const int fd = open_video_device(dirfd(dev_dir), name, st);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
|
||||
const mode_t original_mode = st.st_mode & 07777;
|
||||
const std::string record = name + " " +
|
||||
std::to_string(static_cast<unsigned int>(major(st.st_rdev))) + " " +
|
||||
std::to_string(static_cast<unsigned int>(minor(st.st_rdev))) + " " +
|
||||
[&]() {
|
||||
std::ostringstream out;
|
||||
out << std::oct << static_cast<unsigned int>(original_mode);
|
||||
return out.str();
|
||||
}() + "\n";
|
||||
|
||||
const ssize_t written = write(state_fd, record.data(), record.size());
|
||||
if (written == static_cast<ssize_t>(record.size()) && fchmod(fd, 0000) == 0)
|
||||
++blocked;
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
closedir(dev_dir); // closes dev_fd
|
||||
fsync(state_fd);
|
||||
close(state_fd);
|
||||
|
||||
if (blocked == 0)
|
||||
unlink(kStateFile);
|
||||
|
||||
// No matching device is an idempotent no-op, not an operational failure.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool restore_legacy_v4l2_modes()
|
||||
{
|
||||
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dev_fd < 0)
|
||||
return false;
|
||||
|
||||
DIR* dev_dir = fdopendir(dev_fd);
|
||||
if (!dev_dir) {
|
||||
close(dev_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
while (dirent* entry = readdir(dev_dir)) {
|
||||
const std::string name(entry->d_name);
|
||||
struct stat st {};
|
||||
const int fd = open_video_device(dirfd(dev_dir), name, st);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
|
||||
if ((st.st_mode & 07777) == 0000)
|
||||
fchmod(fd, 0660);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
closedir(dev_dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool restore_v4l2_devices()
|
||||
{
|
||||
const int state_fd = open(kStateFile, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
||||
if (state_fd < 0) {
|
||||
if (errno == ENOENT)
|
||||
return restore_legacy_v4l2_modes();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat state_st {};
|
||||
if (fstat(state_fd, &state_st) != 0 ||
|
||||
!S_ISREG(state_st.st_mode) ||
|
||||
state_st.st_uid != 0 ||
|
||||
state_st.st_nlink != 1 ||
|
||||
(state_st.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
|
||||
close(state_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE* state = fdopen(state_fd, "r");
|
||||
if (!state) {
|
||||
close(state_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dev_fd < 0) {
|
||||
fclose(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
char* line = nullptr;
|
||||
std::size_t capacity = 0;
|
||||
while (getline(&line, &capacity, state) >= 0) {
|
||||
std::istringstream record(line);
|
||||
std::string name;
|
||||
unsigned int expected_major = 0;
|
||||
unsigned int expected_minor = 0;
|
||||
std::string mode_text;
|
||||
std::string extra;
|
||||
|
||||
if (!(record >> name >> expected_major >> expected_minor >> mode_text) ||
|
||||
(record >> extra) ||
|
||||
!is_video_device_name(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned long parsed_mode = 0;
|
||||
try {
|
||||
std::size_t consumed = 0;
|
||||
parsed_mode = std::stoul(mode_text, &consumed, 8);
|
||||
if (consumed != mode_text.size() || parsed_mode > 07777)
|
||||
continue;
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
|
||||
struct stat st {};
|
||||
const int fd = open_video_device(dev_fd, name, st);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
|
||||
if (major(st.st_rdev) == expected_major &&
|
||||
minor(st.st_rdev) == expected_minor) {
|
||||
fchmod(fd, static_cast<mode_t>(parsed_mode));
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
free(line);
|
||||
close(dev_fd);
|
||||
fclose(state); // closes state_fd
|
||||
unlink(kStateFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const uid_t real_uid = getuid();
|
||||
const uid_t effective_uid = geteuid();
|
||||
const CallerIdentity caller = resolve_caller_identity();
|
||||
|
||||
// A setuid installation is explicitly unsupported. Privilege must be
|
||||
// granted by pkexec/sudo so the authorization step remains visible.
|
||||
if (effective_uid == 0 && real_uid != 0) {
|
||||
std::cerr << "[Privacy] Refusing setuid execution; use pkexec or sudo\n";
|
||||
return 11;
|
||||
}
|
||||
|
||||
clearenv();
|
||||
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
|
||||
if (geteuid() != 0) {
|
||||
std::cerr << _("Errore: questo helper deve essere eseguito come root.\n");
|
||||
|
||||
if (effective_uid != 0) {
|
||||
std::cerr << "[Privacy] This helper must be executed as root via pkexec or sudo\n";
|
||||
return 10;
|
||||
}
|
||||
|
||||
|
|
@ -158,31 +560,20 @@ int main(int argc, char* argv[]) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
std::string cmd = argv[1];
|
||||
bool ok = false;
|
||||
const std::string action(argv[1]);
|
||||
|
||||
if (has_pipewire()) {
|
||||
if (cmd == "block") {
|
||||
ok = pipewire_disable_camera();
|
||||
} else if (cmd == "unblock") {
|
||||
ok = pipewire_enable_camera();
|
||||
} else {
|
||||
std::cerr << _("Comando non valido: ") << cmd << "\n";
|
||||
return 2;
|
||||
}
|
||||
} else {
|
||||
if (cmd == "block") {
|
||||
block_v4l2_devices();
|
||||
ok = true;
|
||||
} else if (cmd == "unblock") {
|
||||
unblock_v4l2_devices();
|
||||
ok = true;
|
||||
} else {
|
||||
std::cerr << _("Comando non valido: ") << cmd << "\n";
|
||||
return 2;
|
||||
}
|
||||
if (action == "block") {
|
||||
if (set_pipewire_camera_state(caller, true))
|
||||
return 0;
|
||||
return block_v4l2_devices() ? 0 : 5;
|
||||
}
|
||||
|
||||
return ok ? 0 : 5;
|
||||
}
|
||||
if (action == "unblock") {
|
||||
const bool pipewire_ok = set_pipewire_camera_state(caller, false);
|
||||
const bool v4l2_ok = restore_v4l2_devices();
|
||||
return (pipewire_ok || v4l2_ok) ? 0 : 5;
|
||||
}
|
||||
|
||||
std::cerr << "[Privacy] Invalid command: " << action << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue