Add /get_version, based on openmonero with a few extra additions (#209)
What changed, and why it matters
This commit adds a new public /get_version endpoint to the Monero Light Wallet Server. It also changes the server to accept GET requests for some endpoints and records the HTTP method used. The new endpoint exposes detailed version and build information, including the exact git commit hash, branch, build date, Monero version, blockchain height, and server configuration. This information could help an attacker identify outdated or vulnerable server versions, but the commit itself does not appear to introduce a direct exploit.
Operators should review whether the /get_version metadata exposure is acceptable for their threat model. If version fingerprinting is a concern, consider restricting access to the endpoint or reducing the fields returned. Review that allowing GET requests does not bypass any endpoint-specific assumptions about request bodies or idempotency.
Security signals we found
New public endpoint exposes detailed software version and build metadata
Git commit hash, branch name, and build date disclosed to unauthenticated callers
Server configuration details (max_subaddresses, network type, testnet flag) exposed
HTTP verb handling changed to allow GET requests for endpoints previously requiring POST
No authentication required for /get_version (registered with login_required=false)
Evidence from the diff
The patch introduces a new REST endpoint /get_version that returns a get_version_response containing server_type, server_version, last_git_commit_hash, last_git_commit_date, git_branch_name, monero_version_full, blockchain_height, api version, max_subaddresses, network_type, and testnet flag. It modifies the request dispatch logic to allow GET requests (previously only POST was accepted) and to skip JSON body parsing for empty request types. A new error code bad_verb is added for incorrect HTTP methods. The version metadata is generated at build time from git via CMake configure_file.
Changed components
src/rest_server.cppsrc/rpc/light_wallet.cppsrc/rpc/light_wallet.hsrc/CMakeLists.txtsrc/lws_version.h.insrc/error.cppsrc/error.hInspect captured patch +188 / −7
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1ddc24c..92d5401 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -28,6 +28,35 @@
include_directories(.)
+find_package(Git)
+
+set(MLWS_COMMIT_BRANCH "")
+set(MLWS_COMMIT_DATE "")
+set(MLWS_COMMIT_HASH "")
+if (GIT_FOUND)
+ execute_process(
+ COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ OUTPUT_VARIABLE MLWS_COMMIT_HASH
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+ execute_process(
+ COMMAND "${GIT_EXECUTABLE}" rev-parse --abbrev-ref HEAD
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ OUTPUT_VARIABLE MLWS_COMMIT_BRANCH
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+ execute_process(
+ COMMAND "${GIT_EXECUTABLE}" log -1 "--date=format:\"%Y/%m/%d %T\"" "--format=\"%ad\""
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ OUTPUT_VARIABLE MLWS_COMMIT_DATE
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+endif ()
+
+configure_file(lws_version.h.in "${CMAKE_BINARY_DIR}/generated_include/lws_version.h")
+include_directories("${CMAKE_BINARY_DIR}/generated_include")
+
add_subdirectory(lmdb)
add_subdirectory(wire)
add_subdirectory(db)
diff --git a/src/error.cpp b/src/error.cpp
index 39f8fd6..a4db6c9 100644
--- a/src/error.cpp
+++ b/src/error.cpp
@@ -61,6 +61,8 @@ namespace lws
return "Invalid blockchain height";
case error::bad_url:
return "Invlaid URL";
+ case error::bad_verb:
+ return "Incorrect HTTP verb provided";
case error::bad_webhook:
return "Invalid webhook request";
case error::blockchain_reorg:
diff --git a/src/error.h b/src/error.h
index fcc9cc1..ca67de3 100644
--- a/src/error.h
+++ b/src/error.h
@@ -44,6 +44,7 @@ namespace lws
bad_daemon_response, //!< RPC Response from daemon was invalid
bad_height, //!< Invalid blockchain height
bad_url, //!< Invalid URL
+ bad_verb, //!< Bad HTTP verb for endpoint
bad_webhook, //!< Invalid webhook request
blockchain_reorg, //!< Blockchain reorg after fetching/scanning block(s)
configuration, //!< Process configuration invalid
diff --git a/src/lws_version.h.in b/src/lws_version.h.in
new file mode 100644
index 0000000..e5f0146
--- /dev/null
+++ b/src/lws_version.h.in
@@ -0,0 +1,47 @@
+// Copyright (c) 2025, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#pragma once
+
+#include <cstdint>
+
+namespace lws { namespace version
+{
+ constexpr const char branch[] = "@MLWS_COMMIT_BRANCH@";
+ constexpr const char commit[] = "@MLWS_COMMIT_HASH@";
+ constexpr const char date[] = "@MLWS_COMMIT_DATE@";
+ constexpr const char id[] = "0.4-alpha";
+ constexpr const char name[] = "monero-lws";
+
+ // openmonero is currently on 1.6 and we have multiple additions since then
+ namespace api
+ {
+ constexpr const std::uint16_t major = 1;
+ constexpr const std::uint16_t minor = 7;
+ constexpr const std::uint32_t combined = std::uint32_t(major) << 16 | minor;
+ }
+}} // lws // version
+
diff --git a/src/rest_server.cpp b/src/rest_server.cpp
index 859a9cc..f27f9b2 100644
--- a/src/rest_server.cpp
+++ b/src/rest_server.cpp
@@ -159,10 +159,11 @@ namespace lws
struct connection_data
{
rest_server_data* const global; //!< Valid for lifetime of server
+ boost::beast::http::verb last_verb;
bool passed_login; //!< True iff a login via viewkey was successful
explicit connection_data(rest_server_data* global) noexcept
- : global(global), passed_login(false)
+ : global(global), last_verb(boost::beast::http::verb::unknown), passed_login(false)
{}
//! \return Next request timeout, based on login status
@@ -300,7 +301,7 @@ namespace lws
using response = epee::byte_slice; // sometimes async
using async_response = rpc::daemon_status_response;
- static expect<response> handle(const request&, const connection_data& data, std::function<async_complete>&& resume)
+ static expect<response> handle(request, const connection_data& data, std::function<async_complete>&& resume)
{
using info_rpc = cryptonote::rpc::GetInfo;
@@ -1303,6 +1304,33 @@ namespace lws
}
};
+ struct get_version
+ {
+ using request = rpc::get_version_request;
+ using response = rpc::get_version_response;
+
+ static expect<response> handle(request, const connection_data& data, std::function<async_complete>&&)
+ {
+ lws::db::block_id height{};
+ {
+ auto reader = data.global->disk.start_read();
+ if (reader)
+ {
+ auto db_height = reader->get_last_block();
+ if (db_height)
+ height = db_height->id;
+ else
+ MWARNING("Failed to get DB height: " << db_height.error().message());
+ }
+ else
+ MWARNING("Failed to start db reader: " << reader.error().message());
+ }
+
+ // response constructor fills remaining fields
+ return response{height, data.global->options.max_subaddresses};
+ }
+ };
+
struct import_request
{
using request = rpc::import_request;
@@ -1710,11 +1738,16 @@ namespace lws
throw std::logic_error{"async REST handler not setup properly"};
if (std::is_same<epee::byte_slice, response>() && !resume)
throw std::logic_error{"async REST handler not setup properly"};
-
+
request req{};
- std::error_code error = wire::json::from_bytes(std::move(root), req);
- if (error)
- return error;
+ if (!std::is_empty<request>())
+ {
+ if (data.last_verb != boost::beast::http::verb::post)
+ return {error::bad_verb};
+ std::error_code error = wire::json::from_bytes(std::move(root), req);
+ if (error)
+ return error;
+ }
expect<response> resp = E::handle(std::move(req), data, std::move(resume));
if (!resp)
@@ -1744,6 +1777,9 @@ namespace lws
expect<epee::byte_slice> call_admin(std::string&& root, connection_data& data, std::function<async_complete>&&)
{
using request = typename E::request;
+
+ if (data.last_verb != boost::beast::http::verb::post)
+ return {error::bad_verb};
admin<request> req{};
{
@@ -1804,6 +1840,7 @@ namespace lws
{"/get_subaddrs", call<get_subaddrs>, 2 * 1024, false},
{"/get_txt_records", nullptr, 0, false},
{"/get_unspent_outs", call<get_unspent_outs>, 2 * 1024, true},
+ {"/get_version", call<get_version>, 1024, false},
{"/import_wallet_request", call<import_request>, 2 * 1024, false},
{"/login", call<login>, 2 * 1024, false},
{"/provision_subaddrs", call<provision_subaddrs>, 2 * 1024, false},
@@ -2010,6 +2047,8 @@ namespace lws
assert(strand_.running_in_this_thread());
if (error.category() == wire::error::rapidjson_category() || error == lws::error::invalid_range || error == lws::error::not_enough_amount)
return bad_request(boost::beast::http::status::bad_request, std::forward<F>(resume));
+ else if (error == lws::error::bad_verb)
+ return bad_request(boost::beast::http::status::method_not_allowed, std::forward<F>(resume));
else if (error == lws::error::account_not_found || error == lws::error::duplicate_request)
return bad_request(boost::beast::http::status::forbidden, std::forward<F>(resume));
else if (error == lws::error::max_subaddresses)
@@ -2119,7 +2158,8 @@ namespace lws
return self_->bad_request(boost::beast::http::status::bad_request, std::forward<F>(resume));
}
- if (self_->parser_->get().method() != boost::beast::http::verb::post)
+ const boost::beast::http::verb verb = self_->parser_->get().method();
+ if (verb != boost::beast::http::verb::post && verb != boost::beast::http::verb::get)
return self_->bad_request(boost::beast::http::status::method_not_allowed, std::forward<F>(resume));
std::function<async_complete> resumer;
@@ -2143,6 +2183,7 @@ namespace lws
}
MDEBUG("Running REST handler " << handler->name << " on " << self_.get());
+ self_->data_.last_verb = verb;
auto body = handler->run(std::move(self_->parser_->get()).body(), self_->data_, std::move(resumer));
if (!body)
return self_->bad_request(body.error(), std::forward<F>(resume));
diff --git a/src/rpc/light_wallet.cpp b/src/rpc/light_wallet.cpp
index adc83bc..3ab20c9 100644
--- a/src/rpc/light_wallet.cpp
+++ b/src/rpc/light_wallet.cpp
@@ -37,10 +37,12 @@
#include "config.h"
#include "db/string.h"
#include "error.h"
+#include "lws_version.h"
#include "time_helper.h" // monero/contrib/epee/include
#include "ringct/rctOps.h" // monero/src
#include "span.h" // monero/contrib/epee/include
#include "util/random_outputs.h"
+#include "version.h" // monero/src
#include "wire.h"
#include "wire/adapted/crypto.h"
#include "wire/error.h"
@@ -362,6 +364,36 @@ namespace lws
);
}
+ rpc::get_version_response::get_version_response(const db::block_id height, const std::uint32_t max_subaddresses)
+ : server_type(lws::version::name),
+ server_version(lws::version::id),
+ last_git_commit_hash(lws::version::commit),
+ last_git_commit_date(lws::version::date),
+ git_branch_name(lws::version::branch),
+ monero_version_full(MONERO_VERSION_FULL),
+ blockchain_height(height),
+ api(lws::version::api::combined),
+ max_subaddresses(max_subaddresses),
+ network(lws::rpc::network_type(lws::config::network)),
+ testnet(config::network == cryptonote::TESTNET)
+ {}
+ void rpc::write_bytes(wire::json_writer& dest, const get_version_response& self)
+ {
+ wire::object(dest,
+ WIRE_FIELD(server_type),
+ WIRE_FIELD(server_version),
+ WIRE_FIELD(last_git_commit_hash),
+ WIRE_FIELD(last_git_commit_date),
+ WIRE_FIELD(git_branch_name),
+ WIRE_FIELD(monero_version_full),
+ WIRE_FIELD_COPY(blockchain_height),
+ WIRE_FIELD(api),
+ WIRE_FIELD_COPY(max_subaddresses),
+ wire::field("network_type", self.network),
+ WIRE_FIELD_COPY(testnet)
+ );
+ }
+
void rpc::read_bytes(wire::json_reader& source, import_request& self)
{
std::string address;
diff --git a/src/rpc/light_wallet.h b/src/rpc/light_wallet.h
index dc78199..5d6ffb8 100644
--- a/src/rpc/light_wallet.h
+++ b/src/rpc/light_wallet.h
@@ -224,6 +224,35 @@ namespace rpc
void write_bytes(wire::json_writer&, const get_subaddrs_response&);
+ struct get_version_request
+ {
+ get_version_request() = delete;
+ };
+ inline void read_bytes(const wire::reader&, const get_version_request&)
+ {}
+
+ struct get_version_response
+ {
+ //! Defaults to current network in unavailable state
+ get_version_response(lws::db::block_id height, std::uint32_t max_subaddresses);
+
+
+ const std::string server_type;
+ const std::string server_version;
+ const std::string last_git_commit_hash;
+ const std::string last_git_commit_date;
+ const std::string git_branch_name;
+ const std::string monero_version_full;
+
+ const db::block_id blockchain_height;
+ const std::uint32_t api;
+ const std::uint32_t max_subaddresses;
+ const network_type network;
+ const bool testnet;
+ };
+ void write_bytes(wire::json_writer&, const get_version_response&);
+
+
struct import_request
{
import_request() = delete;
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.