From caf3fc9df817bdc5a0ab955a0ed64b563fe50812 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Wed, 21 Oct 2026 17:22:39 -0300 Subject: [PATCH 1/2] listenHTTP: bind to the given host, localhost by default Warn when the server is reachable from the network, refuse systemExec over HTTP, and keep the file server out of .. and absolute paths. Reported by @HugoFara. --- CHANGELOG.md | 5 +++++ src/Command.cpp | 18 +++++++++--------- src/HttpCommandServer.hpp | 31 +++++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1724b70b..f609cd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ Merged since v2.5.0, not yet released. the wheel. ([#154]) - Free-threaded (`cp314t`) wheels are no longer published, because the core is not yet thread-safe; CPython 3.14 is declared supported. ([#155]) +- `listenHTTP[]` binds to `localhost` by default and to the host given in + `listenHTTP[host:port]`; use `listenHTTP[0.0.0.0:8000]` for the network, + which prints a warning. + `systemExec` is refused over HTTP, and the file server stays inside the run + directory and `FOREFIREHOME/tools/htdocs`. ## [v2.5.0] — 2026-08-11 diff --git a/src/Command.cpp b/src/Command.cpp index d2509161..d136a3ec 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -3368,6 +3368,10 @@ namespace libforefire std::string Command::executeCommandAndCaptureOutput(const std::string &cmd) { // 'cmd' is assumed to be the command text after the "ff:" prefix. + // No shell commands from the network: same name parsing as ExecuteCommand. + if (removeTabs(cmd.substr(0, cmd.find('['))) == "systemExec") + return "systemExec is not available over HTTP\n"; + std::streambuf *oldbuf = std::cout.rdbuf(); std::ostringstream captured; std::cout.rdbuf(captured.rdbuf()); @@ -3407,14 +3411,10 @@ namespace libforefire } } - char hostname[256]; - if (gethostname(hostname, sizeof(hostname)) != 0) - { - cout << "Cannot get hostname info " << endl; - return error; - } - - std::string address = arg.empty() ? std::string(hostname) + ":" + std::to_string(port) : arg; + // Local only by default; listenHTTP[0.0.0.0:8000] to open it to the network. + std::string address = arg.empty() ? defaultHostname + ":" + std::to_string(port) : arg; + if (address.compare(0, 5, "host=") == 0) + address = address.substr(5); // Allocate a new HTTP server if none exists. if (currentSession.server == 0) @@ -3434,7 +3434,7 @@ namespace libforefire std::ofstream infoFile("httpConnectInfo.txt"); if (infoFile) { - infoFile << hostname << ":" << port << std::endl; + infoFile << address << std::endl; infoFile.close(); } else diff --git a/src/HttpCommandServer.hpp b/src/HttpCommandServer.hpp index 5708462a..134d0821 100644 --- a/src/HttpCommandServer.hpp +++ b/src/HttpCommandServer.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace http_command { @@ -47,6 +48,20 @@ namespace http_command { return false; } int port = std::stoi(address.substr(colon + 1)); + std::string host = address.substr(0, colon); + + // Bind to the requested host only (0.0.0.0 for all interfaces). + addrinfo hints; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + addrinfo *resolved = nullptr; + if (getaddrinfo(host.c_str(), nullptr, &hints, &resolved) != 0 || resolved == nullptr) { + std::cerr << "Cannot resolve host " << host << std::endl; + return false; + } + in_addr bindAddr = reinterpret_cast(resolved->ai_addr)->sin_addr; + freeaddrinfo(resolved); server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { @@ -59,7 +74,7 @@ namespace http_command { sockaddr_in addr; std::memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_addr = bindAddr; addr.sin_port = htons(port); if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { perror("bind"); @@ -73,10 +88,18 @@ namespace http_command { server_thread = std::thread(&HttpCommandServer::acceptLoop, this); std::cout << "\033[35m" // start color - << "ForeFire HTTP command server listening at http://localhost:" << port + << "ForeFire HTTP command server listening at http://" << host << ":" << port << "\033[0m" << std::endl; + if ((ntohl(bindAddr.s_addr) >> 24) != 127) { + std::cout + << "\033[31m" // start red + << "WARNING: development server, reachable from the network. Anyone who can reach this port can run ForeFire commands. Use listenHTTP[localhost:" << port << "] unless you need remote access." + << "\033[0m" + << std::endl; + } + std::cout << "\033[33m" // start yellow << "To stop the server, press Ctrl-C or type quit in the ForeFire shell." @@ -269,6 +292,10 @@ namespace http_command { if (!path.empty() && path[0] == '/') path = path.substr(1); + // Only files under the run directory or FOREFIREHOME/tools/htdocs. + if ((!path.empty() && path[0] == '/') || ("/" + path + "/").find("/../") != std::string::npos) + return buildSimpleResponse("403 Forbidden", "Forbidden path: " + uri); + // For an empty path, try current directory's index.html; // if not available, check the FOREFIREHOME environment variable. if (path.empty()) { From 99f5bb4a550d66342d658b7834bb84805277b976 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Wed, 21 Oct 2026 17:44:47 -0300 Subject: [PATCH 2/2] listenHTTP: refuse systemExec for any command coming from HTTP Check in systemExec itself, so include[] and @t= events scheduled over HTTP are covered too. --- src/Command.cpp | 14 ++++++++++---- src/Command.h | 3 +++ src/EventCommand.cpp | 4 ++++ src/EventCommand.h | 3 ++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Command.cpp b/src/Command.cpp index d136a3ec..2e02f540 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -29,6 +29,7 @@ namespace libforefire double Command::endTime = 0; bool Command::firstCommand = true; + thread_local bool Command::remoteCommand = false; size_t Command::refTabs = 0; FFPoint *Command::lastReadLoc = 0; @@ -1184,6 +1185,13 @@ namespace libforefire } int Command::systemExec(const string &arg, size_t &numTabs) { + // No shell commands from the network, even through include[] or @t=. + if (remoteCommand) + { + cout << "systemExec is not available over HTTP" << endl; + return error; + } + SimulationParameters *simParam = SimulationParameters::GetInstance(); string finalStr; @@ -3368,16 +3376,14 @@ namespace libforefire std::string Command::executeCommandAndCaptureOutput(const std::string &cmd) { // 'cmd' is assumed to be the command text after the "ff:" prefix. - // No shell commands from the network: same name parsing as ExecuteCommand. - if (removeTabs(cmd.substr(0, cmd.find('['))) == "systemExec") - return "systemExec is not available over HTTP\n"; - std::streambuf *oldbuf = std::cout.rdbuf(); std::ostringstream captured; std::cout.rdbuf(captured.rdbuf()); std::string fireCommand = cmd; // Execute the command. (Assuming ExecuteCommand is defined elsewhere.) + remoteCommand = true; ExecuteCommand(fireCommand); + remoteCommand = false; std::cout.rdbuf(oldbuf); return captured.str(); diff --git a/src/Command.h b/src/Command.h index bd0f8b36..7387fc55 100644 --- a/src/Command.h +++ b/src/Command.h @@ -232,6 +232,9 @@ class Command { public: + // True while running a command received over HTTP (or scheduled by one) + static thread_local bool remoteCommand; + // Reference time static double refTime; diff --git a/src/EventCommand.cpp b/src/EventCommand.cpp index ca686902..099a4d87 100644 --- a/src/EventCommand.cpp +++ b/src/EventCommand.cpp @@ -19,6 +19,7 @@ namespace libforefire{ EventCommand::EventCommand(string scommand, double schedueledTime) : ForeFireAtom(schedueledTime) { schedueledCommand = scommand; + remote = Command::remoteCommand; }; @@ -29,7 +30,10 @@ EventCommand::~EventCommand() {}; void EventCommand::update(){ + bool caller = Command::remoteCommand; + Command::remoteCommand = remote; Command::ExecuteCommand(schedueledCommand); + Command::remoteCommand = caller; } void EventCommand::timeAdvance(){ setUpdateTime(numeric_limits::infinity()); diff --git a/src/EventCommand.h b/src/EventCommand.h index e2d947b2..d3fb6ae1 100644 --- a/src/EventCommand.h +++ b/src/EventCommand.h @@ -25,10 +25,11 @@ namespace libforefire{ class EventCommand: public ForeFireAtom { string schedueledCommand; + bool remote; // scheduled by a command received over HTTP public: /*! \brief Default constructor */ - EventCommand() : ForeFireAtom(0.) {}; + EventCommand() : ForeFireAtom(0.), remote(false) {}; /*! \brief standard constructor */ EventCommand( string, double ) ; /*! \brief Default destructor */