#40530 [W&A-High] JWT Salt Expiration isn't entirely correct in wallet_rpc_server::auth_http_request
Submitted on Feb 26th 2025 at 01:32:59 UTC by @jovi for IOP | Zano
Report ID: #40530
Report Type: Websites and Applications
Report severity: High
Target: https://github.com/immunefi-team/zano-iop/blob/main/src/wallet/wallet_rpc_server.cpp
Impacts:
Taking and/modifying authenticated actions (with or without blockchain state interaction) on behalf of other users without any interaction by that user, such as:
Changing registration information
Commenting
Voting
Making trades
Withdrawals, etc.
Malicious interactions with an already-connected wallet, such as:
Modifying transaction arguments or parameters
Substituting contract addresses
Submitting malicious transactions
Description
Summary
A logic flaw in how the server calculates salt expiration allows replay of JWT tokens in Zano's Wallet RPC. Specifically, the code uses epee::misc_utils::get_tick_count()
—which returns milliseconds—to determine ticks_now
, but the salt’s retention duration (JWT_TOKEN_EXPIRATION_MAXIMUM
) is defined in seconds. This mismatch causes the server to “forget” salts early, re-enabling replayed tokens.
Vulnerability Details
Location
wallet_rpc_server.cpp
, functionbool wallet_rpc_server::auth_http_request(...)
.The relevant time function is defined in
epee::misc_utils::get_tick_count()
, which derives its return value in milliseconds:In
wallet_rpc_server::auth_http_request
, that ticks value is read intoticks_now
:Because
JWT_TOKEN_EXPIRATION_MAXIMUM
is60 * 60
(seconds) instead of60 * 60 * 1000
(milliseconds), salts are purged prematurely.
Description
The server stores each token’s
salt
for a certain “expiration” window to prevent replay attacks (i.e., a second use of the same token).If
JWT_TOKEN_EXPIRATION_MAXIMUM
is 3600 andget_tick_count()
is in milliseconds, the code will only keep salts for ~3.6 seconds (instead of one hour).After the server’s next request-based cleanup, a just-used token’s salt is dropped from memory far too soon, inadvertently allowing the token to be reused.
Code Snippets
wallet_rpc_server::auth_http_request
:epee::misc_utils::get_tick_count
:
4. Impact
Operating Context
By default, the wallet RPC server is typically run locally (bound to
127.0.0.1
), which limits exposure under normal configurations. However, if it is exposed to an untrusted network—e.g., on a public interface or open Wi-Fi—this vulnerability becomes much more dangerous.
Attack Complexity
An attacker needs to sniff the unencrypted (HTTP) traffic and intercept a valid JWT token. No special privileges or direct code execution is required—just local network access or the ability to position oneself on‐path.
High Potential Impact
Replay Attacks:
Due to the premature salt removal, JWT tokens become valid again after ~3.6 seconds. The attacker can resend them indefinitely, bypassing replay protection entirely.
Unauthorized parties can use replayed tokens to invoke protected RPC endpoints (e.g., transfer funds).
While the server’s secret key itself is not disclosed, replay defeats the intended “one‐time use” property of the token.
Complete Wallet Control:
Because the RPC enables critical operations (transfers, address management, and more), replaying a stolen token effectively grants full control of the wallet.
Funds can be stolen or manipulated without any further user interaction.
Proof of Concept
Intercept a valid JWT from a legitimate request to the Wallet RPC.
Wait for about 4–5 seconds (or until any subsequent request triggers salt cleanup).
Replay the same token: the server sees no record of the salt (purged) and permits the exact same request again.
Especifically for this program: runnable PoC code is not required. Whitehats are instead required to write a step-by-step explanation of the PoC and impact.
Was this helpful?