strstr etc

This commit is contained in:
2026-06-01 19:40:20 +02:00
parent 379e9da6c2
commit 7540b6f1ce
2 changed files with 96 additions and 9 deletions
@@ -6138,19 +6138,38 @@ void StructCreature::checkAdditionalItemEffect()
// Fraun Sky Accessories 7/12/2025
// Fix: the bonus is stored as (bitset, base, per-level) triples - the same layout the
// engine uses for EF_PARAMETER_INC (see _applyEffectForState: incParameter(fValue[k],
// fValue[k+1] + fValue[k+2]*level, ...)) and the client tooltip
// (ReplaceAdditionalItemEffectOptionTooltip walks value[n]/[n+1]/[n+2], stride 3).
// The old loop walked the triples but always read fValue[1]/[2] of triple 0, passing
// the *amount* as the stat selector bitset and the *per-level* term (normally 0) as the
// value -> worn sky accessories granted nothing even though the tooltip advertised a
// bonus. Apply each non-empty triple the same way the engine does instead.
if (pEquipment->GetItemAdditionalEffect() > 0)
{
const std::vector<EffectInfo* >* Effect = GameContent::GetEffectInfoVector(pEquipment->GetItemAdditionalEffect());
if (Effect->size() > 0)
{
for (int eff = 0; eff < 20; )
EffectInfo * pSkyEffect = Effect->at(0);
// 6 triples fit in fValue[NUMBER_OF_VALUE(=20)]; stop at eff+2 < NUMBER_OF_VALUE
// so fValue[eff+1] / fValue[eff+2] stay in bounds.
for (int eff = 0; eff + 2 < EffectInfo::NUMBER_OF_VALUE; eff += 3)
{
if (Effect->at(0)->fValue[eff] != 0)
{
incParameter(Effect->at(0)->fValue[1], Effect->at(0)->fValue[2], true);
}
eff += 3;
if (pSkyEffect->fValue[eff] == 0)
continue;
const c_fixed10 fAmount = pSkyEffect->fValue[eff + 1] + pSkyEffect->fValue[eff + 2] * pSkyEffect->nEffectLevel;
// fValue[eff] is the stat/attribute selector bitset. Apply it both as a base
// stat (STR/VIT/... under bStat=true) and as an attribute (attack/defence/...
// under bStat=false): the two branches of incParameter() test disjoint flag
// sets, so whichever the effect row encodes is applied exactly once and the
// result matches what the client tooltip shows.
incParameter(pSkyEffect->fValue[eff], fAmount, true);
incParameter(pSkyEffect->fValue[eff], fAmount, false);
}
}
}
+71 -3
View File
@@ -5573,6 +5573,68 @@ void StructPlayer::SetDialogTitle( const char *szString, int type )
m_strSpecialDialogMenu.clear();
}
// --- Fraun security fix: NPC dialog trigger validation -------------------------------
// The client echoes dialog triggers back to the server, which then runs them through
// LUA()->RunString() (see onDialog in GameMessage.cpp). Validation used to be a substring
// test (strstr), so a crafted / man-in-the-middled TS_CS_DIALOG packet could embed a
// legitimate trigger substring and append arbitrary Lua, e.g.
// on_channel_set(1) os.execute("...")
// Because the Lua VM is opened with luaL_openlibs (os/io included), that is full remote
// code execution on the server host. The validators below now constrain the trigger to
// exactly the shapes the real client can produce.
//
// A "special" dialog menu is a parameterised trigger: the server hands out a prefix
// (e.g. "on_channel_set", "warp_to_secret_dungeon") and the client completes it with a
// numeric argument list -> "prefix(<numbers>)" (client builds it as "%s(%s)" / "%s()" in
// SUIInputNumberWnd / SGameInterface). This helper accepts ONLY that shape: the trigger
// must start with the prefix and the remainder may contain nothing but a single
// parenthesised list of digits / signs / dots / commas / whitespace. That lets the client
// pick a number while making it impossible to chain extra Lua statements.
static bool IsSafeSpecialDialogTrigger( const char * szTrigger, const char * szPrefix )
{
if( !szTrigger || !szPrefix || !*szPrefix )
return false;
const size_t nPrefixLen = strlen( szPrefix );
// Must START with the offered prefix (not merely contain it).
if( strncmp( szTrigger, szPrefix, nPrefixLen ) != 0 )
return false;
const char * p = szTrigger + nPrefixLen;
while( *p == ' ' || *p == '\t' ) ++p; // optional whitespace before '('
if( *p == '\0' )
return true; // bare prefix, e.g. "exit_indun"
if( *p != '(' )
return false; // anything other than an arg list -> reject
++p;
// Argument list: numbers only. No quotes, letters, ';', '=', '[', a second '(' etc.,
// so no further Lua statement can be smuggled in.
while( *p && *p != ')' )
{
const char c = *p;
const bool bAllowed =
( c >= '0' && c <= '9' ) ||
c == ',' || c == '.' || c == '-' || c == '+' || c == ' ' || c == '\t';
if( !bAllowed )
return false;
++p;
}
if( *p != ')' )
return false; // unterminated argument list
++p;
while( *p == ' ' || *p == '\t' ) ++p; // optional trailing whitespace
return *p == '\0'; // nothing may follow the ')'
}
// -------------------------------------------------------------------------------------
void StructPlayer::SetFixedDialogTrigger( const char *szString )
{
m_strFixedDialogTrigger = szString;
@@ -5580,7 +5642,11 @@ void StructPlayer::SetFixedDialogTrigger( const char *szString )
bool StructPlayer::IsFixedDialogTrigger( const char * szTrigger )
{
if( m_strFixedDialogTrigger.size() && strstr( szTrigger, m_strFixedDialogTrigger.c_str() ) )
// Fraun security fix: the fixed trigger is a complete, server-generated string that the
// client echoes back verbatim (e.g. "recall_feather( x, y, layer )" -> SUINotifyWnd sends
// it back with "%s"), so it must match EXACTLY. strstr() let a crafted packet keep the
// trigger as a prefix and append Lua after it.
if( m_strFixedDialogTrigger.size() && !strcmp( szTrigger, m_strFixedDialogTrigger.c_str() ) )
return true;
return false;
@@ -5614,7 +5680,8 @@ void StructPlayer::SetSpecialDialogMenu( const char * szString )
bool StructPlayer::IsSpecialDialogMenu( const char * szString )
{
return( m_strSpecialDialogMenu.size() && strstr( szString, m_strSpecialDialogMenu.c_str() ) );
// Fraun security fix: was strstr() (substring) -> now require "<prefix>(<numeric args>)".
return( m_strSpecialDialogMenu.size() && IsSafeSpecialDialogTrigger( szString, m_strSpecialDialogMenu.c_str() ) );
}
void StructPlayer::ClearSpecialDialogMenu()
@@ -5769,7 +5836,8 @@ bool StructPlayer::IsValidTrigger( const char *szTrigger )
if( !strcmp( strTriggerList[idx].c_str(), szTrigger ) ) return true;
}
if( m_strSpecialDialogMenu.size() && strstr( szTrigger, m_strSpecialDialogMenu.c_str() ) )
// Fraun security fix: was strstr() (substring) -> now require "<prefix>(<numeric args>)".
if( m_strSpecialDialogMenu.size() && IsSafeSpecialDialogTrigger( szTrigger, m_strSpecialDialogMenu.c_str() ) )
return true;
return false;