From 7f5412aad241038259ed9aaa55397c3defd2e9ec Mon Sep 17 00:00:00 2001 From: David Kindl Date: Fri, 20 Mar 2026 14:17:34 +0100 Subject: [PATCH 1/5] Ini settings for web identity --- src/game/CServerConfig.cpp | 14 ++++++++++++-- src/game/CServerConfig.h | 5 +++++ src/sphere.ini | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/game/CServerConfig.cpp b/src/game/CServerConfig.cpp index 170b45597..987512dc0 100644 --- a/src/game/CServerConfig.cpp +++ b/src/game/CServerConfig.cpp @@ -736,6 +736,8 @@ enum RC_TYPE RC_VERSION, RC_WALKBUFFER, RC_WALKREGEN, + RC_WEBIDENTITY, + RC_WEBIDENTITYFORCE, RC_WOOLGROWTHTIME, // m_iWoolGrowthTime RC_WOPCOLOR, RC_WOPFONT, @@ -1031,7 +1033,9 @@ const CAssocReg CServerConfig::sm_szLoadKeys[RC_QTY + 1] { "VERSION", { ELEM_VOID, 0 }}, { "WALKBUFFER", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWalkBuffer) }}, { "WALKREGEN", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWalkRegen) }}, - { "WOOLGROWTHTIME", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWoolGrowthTime) }}, + { "WEBIDENTITY", { ELEM_CSTRING, static_castOFFSETOF(CServerConfig,m_sWebIdentity) }}, + { "WEBIDENTITYFORCE", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_sWebIdentityForce) }}, + { "WOOLGROWTHTIME", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWoolGrowthTime) }}, { "WOPCOLOR", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWordsOfPowerColor) }}, { "WOPFONT", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWordsOfPowerFont) }}, { "WOPPLAYER", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_fWordsOfPowerPlayer) }}, @@ -1518,7 +1522,13 @@ bool CServerConfig::r_LoadVal( CScript &s ) case RC_WALKBUFFER: m_iWalkBuffer = s.GetArgVal() * MSECS_PER_TENTH; break; - case RC_MEDITATIONMOVEMENTABORT: + case RC_WEBIDENTITY: + m_sWebIdentity = s.GetArgStr(); + break; + case RC_WEBIDENTITYFORCE: + m_sWebIdentityForce = s.GetArgVal() > 0; + break; + case RC_MEDITATIONMOVEMENTABORT: _fMeditationMovementAbort = s.GetArgVal() > 0 ? true : false; break; default: diff --git a/src/game/CServerConfig.h b/src/game/CServerConfig.h index 810d3045e..64d14b5d1 100644 --- a/src/game/CServerConfig.h +++ b/src/game/CServerConfig.h @@ -458,6 +458,11 @@ extern class CServerConfig : public CResourceHolder bool m_fCUOStatus; // Enable or disable the response to ConnectUO pings bool m_fUOGStatus; // Enable or disable the response to UOGateway pings + // Web identity secret from Classic UO Web Shard management. + CSString m_sWebIdentity; + // Force only clients with web identity. + bool m_sWebIdentityForce; + int64 m_iWalkBuffer; // Walk limiting code: buffer size (in tenths of second). int m_iWalkRegen; // Walk limiting code: regen speed (%) diff --git a/src/sphere.ini b/src/sphere.ini index 077cb5140..326bf01f6 100644 --- a/src/sphere.ini +++ b/src/sphere.ini @@ -1203,6 +1203,12 @@ CUOStatus=1 // If enabled, it returns: Name, Age, Clients, Items, Chars and Memory UOGStatus=1 +// Classic UO web identity. +// Enable by uncommenting and placing your generated secret in shard management. +//WebIdentity=YOUR_SHARD_SECRET +// Set to 1 to reject all clients without WebIdentity secret. +WebIdentityForce=0 + // Add these Resource Sections to the defname list, so that they can be accessible via DEFLIST.* [RESOURCELIST] ITEMDEF From e950b91b260ef45da92edfd02b244eb05bf64852 Mon Sep 17 00:00:00 2001 From: David Kindl Date: Fri, 20 Mar 2026 14:22:11 +0100 Subject: [PATCH 2/5] Allow 256 bytes in prelogin process --- src/network/CNetworkInput.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/CNetworkInput.cpp b/src/network/CNetworkInput.cpp index 57443969e..fac98ddaa 100644 --- a/src/network/CNetworkInput.cpp +++ b/src/network/CNetworkInput.cpp @@ -539,10 +539,10 @@ bool CNetworkInput::processUnknownClientData(CNetState* state, Packet* buffer) fHTTPReq = (uiOrigRemainingLength >= 5 && memcmp(pOrigRemainingData, "GET /", 5) == 0) || (uiOrigRemainingLength >= 6 && memcmp(pOrigRemainingData, "POST /", 6) == 0); } - if (!fHTTPReq && (uiOrigRemainingLength > INT8_MAX)) + if (!fHTTPReq && (uiOrigRemainingLength > UINT8_MAX)) { g_Log.EventWarn("%x:Client connected with a seed length of %u exceeding max length limit of %d, disconnecting.\n", - state->id(), uiOrigRemainingLength, INT8_MAX); + state->id(), uiOrigRemainingLength, UINT8_MAX); return false; } From d4ed136a742b8a8a1cba7db6b6f3baa811116869 Mon Sep 17 00:00:00 2001 From: David Kindl Date: Mon, 23 Mar 2026 17:04:46 +0100 Subject: [PATCH 3/5] Web identity - populate data - validate --- src/game/clients/CClient.h | 12 ++++ src/game/clients/CClientLog.cpp | 6 ++ src/network/receive.cpp | 102 ++++++++++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/game/clients/CClient.h b/src/game/clients/CClient.h index 7b7a587be..265e7364a 100644 --- a/src/game/clients/CClient.h +++ b/src/game/clients/CClient.h @@ -150,6 +150,18 @@ class CClient : public CSObjListRec, public CScriptObj, public CChatChanMember, int64 m_timeLastEventWalk; // Last time we got a walk event from client int64 m_timeNextEventWalk; // Fastwalk prevention: only allow more walk requests after this timer + // Web Identity. + struct CWebIdentity + { + bool m_fReceived{false}; + CSString m_sUserId; + CSString m_sConnectingIp; + CSString m_sExternalAuthProvider; + CSString m_sExternalAuthUsername; + CSString m_sExternalAuthId; + CSString m_sRole; + } m_webIdentity; + // Context of the targetting setup. depends on CLIMODE_TYPE m_Targ_Mode union { diff --git a/src/game/clients/CClientLog.cpp b/src/game/clients/CClientLog.cpp index 24ae4eb17..5e8e69752 100644 --- a/src/game/clients/CClientLog.cpp +++ b/src/game/clients/CClientLog.cpp @@ -848,6 +848,12 @@ bool CClient::xProcessClientSetup( CEvent * pEvent, uint uiLen ) ASSERT( pEvent != nullptr ); ASSERT( uiLen > 0 ); + // Web identity packet. Validation is handled in packet itself. + if (g_Cfg.m_sWebIdentity.IsValid() && pEvent->Default.m_Cmd == XCMD_Spy && uiLen == 149) + { + return true; + } + // Try all client versions on the msg. if ( !m_Crypt.Init( m_net->m_seed, pEvent->m_Raw, uiLen, GetNetState()->isClientKR() ) ) { diff --git a/src/network/receive.cpp b/src/network/receive.cpp index 1106e32a0..3b3bfe6fa 100644 --- a/src/network/receive.cpp +++ b/src/network/receive.cpp @@ -1943,6 +1943,20 @@ bool PacketServerSelect::onReceive(CNetState* net) uint server = readInt16(); + // Web Identity validation. + if (g_Cfg.m_sWebIdentity.IsValid() && g_Cfg.m_sWebIdentityForce) + { + CClient* client = net->getClient(); + ASSERT(client); + + // We did not receive validation packet (0xa4). The packet itself is validated elsewhere. + if (!client->m_webIdentity.m_fReceived) + { + client->addLoginErr(PacketLoginError::BadAuthID); + return false; + } + } + net->getClient()->Login_Relay(server); return true; } @@ -1962,12 +1976,92 @@ PacketSystemInfo::PacketSystemInfo() : Packet(149) bool PacketSystemInfo::onReceive(CNetState* net) { ADDTOCALLSTACK("PacketSystemInfo::onReceive"); - UnreferencedParameter(net); - skip(148); - return true; -} + // Not using web identity. + if (!g_Cfg.m_sWebIdentity.IsValid()) + { + UnreferencedParameter(net); + skip(148); + return true; + } + + // Web identity check. + char clientType[7] = {}; + readStringASCII(clientType, 6, false); + const uint8 version = readByte(); + + // Type or version doesn't match, might not be Web Identity. + if (strcmp(clientType, "CUOWEB") != 0 || version != 1) { + skip(141); + + return true; + } + + // We are using web identity, and we passed the version check. Assign data we received from it. + CClient* client = net->getClient(); + ASSERT(client); + + // Skip timestamp. + skip(4); + + // Size of the variable data (estimated size). + constexpr uint8 size = 30; + + // This is remaining length of Web Identity packet. + int length = 137; + + // Read the received secret and compare it to our secret. + char secret[size]; + readStringNullASCII(secret, length); + length -= static_cast(strlen(secret)); + + // Secret is not valid. + if (strcmp(secret, g_Cfg.m_sWebIdentity) != 0) + { + skip(length); + client->addLoginErr(PacketLoginError::BadAuthID); + return false; + } + + // Client is validated, we can now populate Web Identity data. + CClient::CWebIdentity &Identity = client->m_webIdentity; + Identity.m_fReceived = true; + + char user_id[size]; + readStringNullASCII(user_id, length); + length -= static_cast(strlen(user_id)); + Identity.m_sUserId = user_id; + + char connecting_ip[size]; + readStringNullASCII(connecting_ip, length); + length -= static_cast(strlen(connecting_ip)); + Identity.m_sConnectingIp = connecting_ip; + + char external_auth_provider[size]; + readStringNullASCII(external_auth_provider, length); + length -= static_cast(strlen(external_auth_provider)); + Identity.m_sExternalAuthProvider = external_auth_provider; + + char external_auth_username[size]; + readStringNullASCII(external_auth_username, length); + length -= static_cast(strlen(external_auth_username)); + Identity.m_sExternalAuthUsername = external_auth_username; + + char external_auth_id[size]; + readStringNullASCII(external_auth_id, length); + length -= static_cast(strlen(external_auth_id)); + Identity.m_sExternalAuthId = external_auth_id; + + char role[size]; + readStringNullASCII(role, length); + length -= static_cast(strlen(role)); + Identity.m_sRole = role; + + skip(length); + + return true; +} /*************************************************************************** * From d01166e1cf0da60693c8e25b749e370531beeb5c Mon Sep 17 00:00:00 2001 From: David Kindl Date: Tue, 24 Mar 2026 13:01:50 +0100 Subject: [PATCH 4/5] Fix validation when sending packet from client and server doesn't have one (ignore it) - add message to log about web identity packet - refactor reading variable data from packet --- src/game/CServerConfig.h | 2 +- src/game/clients/CClientLog.cpp | 4 +-- src/network/receive.cpp | 55 ++++++++++++++------------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/game/CServerConfig.h b/src/game/CServerConfig.h index 64d14b5d1..8a25ee82e 100644 --- a/src/game/CServerConfig.h +++ b/src/game/CServerConfig.h @@ -459,7 +459,7 @@ extern class CServerConfig : public CResourceHolder bool m_fUOGStatus; // Enable or disable the response to UOGateway pings // Web identity secret from Classic UO Web Shard management. - CSString m_sWebIdentity; + CSString m_sWebIdentity = {false}; // Force only clients with web identity. bool m_sWebIdentityForce; diff --git a/src/game/clients/CClientLog.cpp b/src/game/clients/CClientLog.cpp index 5e8e69752..e9e6723b1 100644 --- a/src/game/clients/CClientLog.cpp +++ b/src/game/clients/CClientLog.cpp @@ -112,7 +112,7 @@ bool CClient::addLoginErr(byte code) "Timeout / Wrong encryption / Unknown error", "Invalid client version. See the CLIENTVERSION setting in " SPHERE_FILE ".ini", "Invalid character selected (chosen character does not exist)", - "AuthID is not correct. This normally means that the client did not log in via the login server", + "AuthID is not correct. This means that the client did not log in via the login server or Web Identity secret doesn't match", "The account details entered are invalid (username or password is too short, too long or contains invalid characters). This can sometimes be caused by incorrect/missing encryption keys", "The account details entered are invalid (username or password is too short, too long or contains invalid characters). This can sometimes be caused by incorrect/missing encryption keys", "Encryption error: packet length does not match what was expected", @@ -849,7 +849,7 @@ bool CClient::xProcessClientSetup( CEvent * pEvent, uint uiLen ) ASSERT( uiLen > 0 ); // Web identity packet. Validation is handled in packet itself. - if (g_Cfg.m_sWebIdentity.IsValid() && pEvent->Default.m_Cmd == XCMD_Spy && uiLen == 149) + if (pEvent->Default.m_Cmd == XCMD_Spy && uiLen == 149) { return true; } diff --git a/src/network/receive.cpp b/src/network/receive.cpp index 3b3bfe6fa..9fed8d992 100644 --- a/src/network/receive.cpp +++ b/src/network/receive.cpp @@ -2005,19 +2005,17 @@ bool PacketSystemInfo::onReceive(CNetState* net) // Skip timestamp. skip(4); - // Size of the variable data (estimated size). - constexpr uint8 size = 30; + // Buffer for reading data from packet. + char dataBuffer[30]; - // This is remaining length of Web Identity packet. + // Remaining length of Web Identity packet (so we don't eat bytes from another packet). int length = 137; // Read the received secret and compare it to our secret. - char secret[size]; - readStringNullASCII(secret, length); - length -= static_cast(strlen(secret)); + length -= readStringNullASCII(dataBuffer, length); // Secret is not valid. - if (strcmp(secret, g_Cfg.m_sWebIdentity) != 0) + if (strcmp(dataBuffer, g_Cfg.m_sWebIdentity) != 0) { skip(length); client->addLoginErr(PacketLoginError::BadAuthID); @@ -2028,36 +2026,31 @@ bool PacketSystemInfo::onReceive(CNetState* net) CClient::CWebIdentity &Identity = client->m_webIdentity; Identity.m_fReceived = true; - char user_id[size]; - readStringNullASCII(user_id, length); - length -= static_cast(strlen(user_id)); - Identity.m_sUserId = user_id; + // User ID. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sUserId = dataBuffer; - char connecting_ip[size]; - readStringNullASCII(connecting_ip, length); - length -= static_cast(strlen(connecting_ip)); - Identity.m_sConnectingIp = connecting_ip; + // Connecting IP. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sConnectingIp = dataBuffer; - char external_auth_provider[size]; - readStringNullASCII(external_auth_provider, length); - length -= static_cast(strlen(external_auth_provider)); - Identity.m_sExternalAuthProvider = external_auth_provider; + // External Auth Provider. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthProvider = dataBuffer; - char external_auth_username[size]; - readStringNullASCII(external_auth_username, length); - length -= static_cast(strlen(external_auth_username)); - Identity.m_sExternalAuthUsername = external_auth_username; + // External Auth Username. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthUsername = dataBuffer; - char external_auth_id[size]; - readStringNullASCII(external_auth_id, length); - length -= static_cast(strlen(external_auth_id)); - Identity.m_sExternalAuthId = external_auth_id; + // External Auth ID. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthId = dataBuffer; - char role[size]; - readStringNullASCII(role, length); - length -= static_cast(strlen(role)); - Identity.m_sRole = role; + // Role. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sRole = dataBuffer; + // Skip the rest of the packet. skip(length); return true; From c2119f50fdebc15bcf8c7098b61ed84abb5a82f2 Mon Sep 17 00:00:00 2001 From: David Kindl Date: Tue, 24 Mar 2026 13:16:05 +0100 Subject: [PATCH 5/5] Changelog entry --- Changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.txt b/Changelog.txt index b87a946fc..f758cf120 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -4120,3 +4120,10 @@ When setting a property like MORE to the a spell or skill defname, trying to rea - Fixed: Hit chance to paralyzed targets with sphere custom (0) formula is now 80-100% (was 0-10%). - Added: Implemented Hit chance increase / decrease to sphere custom formula (0). - Fixed: Equiping armor and weapon with spelleffect could crash the SphereServer (#1538) + +24-03-2026, Mulambo +- Fixed: issue when web client sent a web identity packet and Sphere didn't let him log in +- Added: Classic UO Web Identity packet validation support (#1357): 2 new sphere.ini settings: `WebIdentity` and `WebIdentityForce`: + - If you add `WebIdentity` value from your Classic UO Web Management, received WebIdentity will be checked and validated + - If you add `WebIdentity` and set `WebIdentityForce` to 1, it will prohibit any client without web identity to log in + - If you don't add `WebIdentity`, behaviour will stay as it was