summaryrefslogtreecommitdiff
path: root/indra/newview/llscripteditorws.cpp
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2026-09-24 13:53:34 -0700
committerRider Linden <rider@lindenlab.com>2026-09-24 13:53:34 -0700
commit020410f964f3be0b85fe62078281a43f75fa9585 (patch)
treedfe647d6458ec8ea33a9f7eac59d359c72240675 /indra/newview/llscripteditorws.cpp
parent546782857fef92f1c745d58e30275138ee1e49f8 (diff)
Issue websocket hardening.
Diffstat (limited to 'indra/newview/llscripteditorws.cpp')
-rw-r--r--indra/newview/llscripteditorws.cpp147
1 files changed, 92 insertions, 55 deletions
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
index 178b03d4da..ddc3e25d7b 100644
--- a/indra/newview/llscripteditorws.cpp
+++ b/indra/newview/llscripteditorws.cpp
@@ -73,6 +73,8 @@
#include <array>
+#include <openssl/rand.h>
+
namespace
{
// Per-operation timeouts (seconds) for coroutine-based async RPC handlers.
@@ -81,6 +83,11 @@ namespace
constexpr F32 NOTECARD_UPLOAD_TIMEOUT = 30.0f;
constexpr F32 ITEM_CREATE_TIMEOUT = 30.0f;
+ // Seconds a client has to answer the handshake: plenty to read a
+ // file, and no longer than that is a connection kept that has not
+ // proven itself.
+ constexpr F64 HANDSHAKE_TIMEOUT = 30.0;
+
// Linkset flush coalescing delays (seconds).
constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f;
constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f;
@@ -1256,7 +1263,7 @@ void LLScriptEditorWSServer::broadcastLanguageChange()
if (isRunning())
{
- broadcastNotification("language.syntax.change", params);
+ notifyAll("language.syntax.change", params);
}
}
}
@@ -2365,7 +2372,7 @@ void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::stri
if (it != mActiveConnections.end())
{
auto connection = it->second.lock();
- if (connection)
+ if (connection && connection->isAuthenticated())
{
connection->notify(method, params);
}
@@ -2385,7 +2392,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa
for (const auto& pair : mActiveConnections)
{
auto connection = pair.second.lock();
- if (connection)
+ if (connection && connection->isAuthenticated())
{
connection->sendMessage(payload);
}
@@ -2816,11 +2823,14 @@ void LLScriptEditorWSConnection::sendHandshake()
handshake["agent_id"] = gAgent.getID();
handshake["agent_name"] = gAgentUsername;
- std::string challenge_file = generateChallenge();
- if (!challenge_file.empty())
+ const Challenge challenge = writeChallenge();
+ if (challenge.mFile.empty())
{
- handshake["challenge"] = challenge_file;
+ // Nothing to prove itself by, so nothing it may do.
+ sendDisconnect(DisconnectReason::INTERNAL_ERROR, "Unable to issue a challenge");
+ return;
}
+ handshake["challenge"] = challenge.mFile;
LLSD languages = LLSD::emptyArray();
languages.append("lsl");
@@ -2839,22 +2849,33 @@ void LLScriptEditorWSConnection::sendHandshake()
wptr_t that = weak_from_this();
- // Send session.handshake method call and the response
- call("session.handshake", handshake, [that](const LLSD& result, const LLSD& error) {
- if (error.isUndefined())
+ // The answer comes once, whichever way: from the client, as the
+ // time runs out, or as the connection closes.
+ const LLSD sent = call(
+ "session.handshake", handshake,
+ [that, challenge](const LLSD& result, const LLSD& error)
{
+ // Whatever the answer, the file has done its work.
+ LLFile::remove(challenge.mFile);
auto self = that.lock();
- if (self)
+ if (!self)
{
- self->handleHandshakeResponse(result);
+ return;
}
- }
- else
- {
- LL_WARNS("ScriptEditorWS") << "Handshake failed: "
- << error["message"].asString() << LL_ENDL;
- }
- });
+ if (error.isDefined())
+ {
+ self->handleHandshakeError(error);
+ return;
+ }
+ self->handleHandshakeResponse(result, challenge.mSecret);
+ },
+ HANDSHAKE_TIMEOUT);
+ if (sent.isUndefined())
+ {
+ LLFile::remove(challenge.mFile);
+ handleHandshakeError(LLSD().with("code", LLJSONRPCConnection::RPCError::INTERNAL_ERROR).with("message", "Handshake not sent"));
+ return;
+ }
LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL;
}
@@ -2886,40 +2907,36 @@ void LLScriptEditorWSConnection::sendDisconnect(DisconnectReason reason, const s
closeConnection(1000, message);
}
-void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
+void LLScriptEditorWSConnection::handleHandshakeError(const LLSD& error)
{
- LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
- LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL;
-
- mClientName = result["client_name"].asString();
- mClientVersion = result["client_version"].asString();
- mProtocolVersion = result["protocol_version"].asString();
-
- // Validate challenge response (if a challenge was issued).
- const bool challenge_issued = mChallenge.notNull();
- bool valid_response = true;
- if (challenge_issued)
+ const S32 code = error["code"].asInteger();
+ if (code == LLJSONRPCConnection::RPCError::CONNECTION_CLOSED)
{
- valid_response = result.has("challenge_response") &&
- (result["challenge_response"].asUUID() == mChallenge);
- mChallenge.setNull();
+ // Gone already.
+ return;
}
+ LL_WARNS("ScriptEditorWS") << "Handshake failed: " << error["message"].asString() << LL_ENDL;
+ sendDisconnect(code == LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT ? DisconnectReason::TIMEOUT : DisconnectReason::PROTOCOL_ERROR,
+ "Handshake failed");
+}
- // Always clean up the temporary challenge file if one was created,
- // regardless of validation outcome.
- if (!mChallengeFile.empty())
- {
- LLFile::remove(mChallengeFile);
- mChallengeFile.clear();
- }
+void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result, const LLUUID& secret)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL;
- if (challenge_issued && !valid_response)
+ // Only something that could read the file knows what it says.
+ if (!result.isMap() || result["challenge_response"].asUUID() != secret)
{
LL_WARNS("ScriptEditorWS") << "Invalid or missing challenge response from client" << LL_ENDL;
sendDisconnect(DisconnectReason::PROTOCOL_ERROR, "Invalid challenge response");
return;
}
+ mClientName = result["client_name"].asString();
+ mClientVersion = result["client_version"].asString();
+ mProtocolVersion = result["protocol_version"].asString();
+
if (mProtocolVersion != "1.0")
{
LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: "
@@ -2945,29 +2962,49 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
}
}
+ // Let in before it is told so: what it asks on hearing it must be
+ // answered.
+ setAuthenticated(true);
notify("session.ok");
LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL;
}
-std::string LLScriptEditorWSConnection::generateChallenge()
+// static
+LLScriptEditorWSConnection::Challenge LLScriptEditorWSConnection::writeChallenge()
{
- mChallenge.generate();
-
- mChallengeFile = std::string(LLFile::tmpdir()) + "sl_script_challenge_" + mChallenge.asString() + ".tmp";
-
- llofstream file(mChallengeFile.c_str());
- if (!file.is_open())
+ // A secret nobody could work out: a new id is made of the time and
+ // the network card's address, which are anybody's to know.
+ Challenge challenge;
+ if (RAND_bytes(challenge.mSecret.mData, UUID_BYTES) != 1)
{
- LL_WARNS("ScriptEditorWS") << "Unable to open challenge file: " << mChallengeFile << LL_ENDL;
- mChallenge.setNull();
- mChallengeFile.clear();
- return std::string();
+ LL_WARNS("ScriptEditorWS") << "Unable to make a challenge secret" << LL_ENDL;
+ return {};
}
- file << mChallenge;
- file.close();
+ // Named for nothing it holds, made new rather than written over
+ // anything, or through a link, already by that name, and readable by
+ // the user alone: the name goes to whoever connects, and the temp
+ // folder can be everyone's.
+ const std::string file = LLFile::tmpdir() + "sl_script_challenge_" + LLUUID::generateNewID().asString() + ".tmp";
+ const std::string text = challenge.mSecret.asString();
+ std::error_code ec;
+ LLFile out(file, LLFile::out | LLFile::noreplace, ec, 0600);
+ if (ec)
+ {
+ // Whatever is there by that name is somebody else's, and stays.
+ LL_WARNS("ScriptEditorWS") << "Unable to make challenge file " << file << ": " << ec.message() << LL_ENDL;
+ return {};
+ }
+ const bool written = out.write(text.data(), static_cast<S64>(text.size()), ec) == static_cast<S64>(text.size()) && !ec;
+ if (out.close(ec) != 0 || !written)
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to write challenge file " << file << LL_ENDL;
+ LLFile::remove(file);
+ return {};
+ }
- return mChallengeFile;
+ challenge.mFile = file;
+ return challenge;
}