From 95ef068dfdfe8c353c360d62e10d03c642de6a8a Mon Sep 17 00:00:00 2001 From: NodesAndNails Date: Sat, 2 May 2026 01:01:00 +1200 Subject: [PATCH] Fix #103: Resolve local privilege escalation in API IPC path Dropping the API IPC file in /tmp (world-writable) let any local user write "install|appId|library" to it, forcing Steam to install arbitrary apps. - Moved the IPC file to $XDG_RUNTIME_DIR with 0600 permissions. - Falls back to ~/.config/SLSsteam if XDG isn't set. - Added lstat() checks on the fallback dir to block symlink traversal and verified ownership to stop cross-UID attacks. - Checked mkdir() return value so we don't silently run in a broken state if it fails. - Added a stat()/geteuid() check in onFileChange() to reject commands if the file owner doesn't match (TOCTOU mitigation). - Added SLSAPI::deinit() to clean up the IPC file. - Wired deinit() into unload() in main.cpp. (Note: Steam's normal exit skips unload(), so XDG tmpfs cleanup on logout is the actual safety net). - Changed path to std::string in api.hpp so we can handle dynamic paths without buffer issues. --- src/api.cpp | 98 +++++++++++++++++++++++++++++++++++++++++++--------- src/api.hpp | 9 +++-- src/main.cpp | 1 + 3 files changed, 86 insertions(+), 22 deletions(-) diff --git a/src/api.cpp b/src/api.cpp index 73d0749..c33f8a0 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -1,17 +1,20 @@ #include "api.hpp" -#include "sdk/IClientAppManager.hpp" - #include "config.hpp" #include "filewatcher.hpp" +#include "sdk/IClientAppManager.hpp" #include "utils.hpp" -#include +#include +#include +#include +#include +#include namespace SLSAPI { - const char* path = "/tmp/SLSsteam.API"; + std::string path; std::fstream fstream; CFileWatcher* watcher; } @@ -23,21 +26,25 @@ bool SLSAPI::isEnabled() void SLSAPI::onFileChange() { - //Hot reload support :) if (!isEnabled()) { return; } - //Shitty way to reopen the stream. We have to do this, otherwise the fstream gets invalidated when running echo > - fstream.close(); - fstream.open(path); + struct stat st; + if (stat(path.c_str(), &st) != 0 || st.st_uid != geteuid()) + { + g_pLog->debug("API rejected file change: ownership mismatch or file missing.\n"); + return; + } + fstream.close(); + fstream.open(path, std::ios::in | std::ios::out); + char cmd[128]; fstream.getline(cmd, sizeof(cmd)); - g_pLog->debug("API Running %s\n", cmd); - + auto split = Utils::strsplit(cmd, "|"); if (strcmp(split[0].c_str(), "install") == 0 && split.size() > 2) { @@ -45,15 +52,12 @@ void SLSAPI::onFileChange() { uint32_t appId = std::strtoul(split[1].c_str(), nullptr, 10); uint32_t library = std::strtoul(split[2].c_str(), nullptr, 10); - if (!g_pClientAppManager) { g_pLog->info("API g_pClientAppManager is nullptr! Aborting...\n"); return; } - g_pLog->info("API Installing %s to %s\n", split[1].c_str(), split[2].c_str()); - g_pClientAppManager->installApp(appId, library); } catch(...) @@ -65,11 +69,71 @@ void SLSAPI::onFileChange() void SLSAPI::init() { - fstream = std::fstream(path, std::ios::in | std::ios::out); + const char* xdg = std::getenv("XDG_RUNTIME_DIR"); + if (xdg && xdg[0] != '\0') + { + path = std::string(xdg) + "/SLSsteam.API"; + } + else + { + const char* home = std::getenv("HOME"); + if (!home) + { + g_pLog->info("API init failed: XDG_RUNTIME_DIR and HOME not set.\n"); + return; + } + + std::string dir = std::string(home) + "/.config/SLSsteam"; + struct stat st; + if (lstat(dir.c_str(), &st) == 0) + { + if (S_ISLNK(st.st_mode)) + { + g_pLog->info("API init failed: fallback directory is a symlink.\n"); + return; + } + if (st.st_uid != geteuid()) + { + g_pLog->info("API init failed: fallback directory not owned by current user.\n"); + return; + } + } + else + { + if (mkdir(dir.c_str(), 0700) != 0) + { + g_pLog->info("API init failed: could not create fallback directory.\n"); + return; + } + } + path = dir + "/SLSsteam.API"; + } + fstream.open(path, std::ios::in | std::ios::out | std::ios::trunc); + chmod(path.c_str(), 0600); + watcher = new CFileWatcher(onFileChange); - watcher->addFile(path); + watcher->addFile(path.c_str()); watcher->start(); - - g_pLog->debug("SLSsteam API initialized!\n"); + + g_pLog->debug("SLSsteam API initialized at %s\n", path.c_str()); } + +void SLSAPI::deinit() +{ + delete watcher; + watcher = nullptr; + + if (fstream.is_open()) + { + fstream.close(); + } + + const char* xdg = std::getenv("XDG_RUNTIME_DIR"); + if (xdg && path.rfind(xdg, 0) == 0) + { + unlink(path.c_str()); + } + + path.clear(); +} \ No newline at end of file diff --git a/src/api.hpp b/src/api.hpp index b531cd4..1e1c912 100644 --- a/src/api.hpp +++ b/src/api.hpp @@ -1,17 +1,16 @@ #pragma once #include - +#include class CFileWatcher; - namespace SLSAPI { - extern const char* path; + extern std::string path; extern std::fstream fstream; extern CFileWatcher* watcher; - bool isEnabled(); void onFileChange(); void init(); -} + void deinit(); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 58765e4..d96dd5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -66,6 +66,7 @@ static bool cleanEnvVar(const char* varName, const char* endsWith) static void unload() { Hooks::remove(); + SLSAPI::deinit(); //This is absolutely unnessecary for applications loading SLSsteam where it cancels from setup() //Would be nice to run have for failed load() attempts though