mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions - Reject MSP/GMCP media file names containing `..` that resolve outside the profile's media directory (relative sub-directories and wildcards still work). - Guard `TEntityResolver::resolveCode()` against the empty value from a malformed numeric entity like `&#;`, which previously called `QString::front()` on an empty string (undefined behaviour). - Add a `TEntityResolver` regression test for the malformed-entity case. #### Motivation for adding to Mudlet A connected game server could use a crafted MSP `!!SOUND`/`!!MUSIC` or GMCP `Client.Media` file name to download and write attacker-controlled content to an arbitrary path (e.g. an autostart entry), or read/play an arbitrary local file — with no user interaction, since both protocols are enabled by default. #### Other info (issues closed, discussion etc) Security hardening of the server-facing media path. `isFileRelative()` only rejected absolute paths, so a relative `..` traversal passed validation before being concatenated onto the media directory. **Test case:** Connect a profile to a server that sends `!!SOUND(../../evil.wav U=http://example/evil.wav)` — it is now rejected with a `WARNING - rejected a media file name that escapes the profile media directory` log line, while a normal `!!SOUND(hit.wav U=...)` still downloads and plays. --- _Generated by [Claude Code](https://claude.ai/code/session_011XUZefuDuy8F1M4bHq5jMZ)_ --------- Signed-off-by: Vadim Peretokin <vperetokin@hey.com> Co-authored-by: Claude <noreply@anthropic.com>
150 lines
4.4 KiB
C++
150 lines
4.4 KiB
C++
|
|
#include <QMap>
|
|
#include <TEntityResolver.h>
|
|
#include <QtTest/QtTest>
|
|
#include "utils.h"
|
|
|
|
class TEntityResolverTest : public QObject {
|
|
Q_OBJECT
|
|
|
|
private:
|
|
|
|
private slots:
|
|
|
|
void initTestCase()
|
|
{
|
|
}
|
|
|
|
void testStandardEntities()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QCOMPARE(resolver.getResolution(" "), " ");
|
|
QCOMPARE(resolver.getResolution(">"), ">");
|
|
QCOMPARE(resolver.getResolution("<"), "<");
|
|
QCOMPARE(resolver.getResolution("&"), "&");
|
|
QCOMPARE(resolver.getResolution("""), "\"");
|
|
QCOMPARE(resolver.getResolution("Ú"), "Ú");
|
|
QCOMPARE(resolver.getResolution("ú"), "ú");
|
|
}
|
|
|
|
void testStandardEntitiesCaseInsensitive()
|
|
{
|
|
TEntityResolver resolver;
|
|
TEntityType type;
|
|
|
|
QCOMPARE(resolver.getResolution(">"), ">");
|
|
QCOMPARE(resolver.getResolution("≪"), "<");
|
|
QCOMPARE(resolver.getResolution("&AmP;"), "&");
|
|
QCOMPARE(resolver.getResolution("""), "\"");
|
|
QCOMPARE(resolver.getResolution("&Copy;", true, &type), "©");
|
|
QCOMPARE(type, ENTITY_TYPE_SYSTEM);
|
|
}
|
|
|
|
void testDecimalCode()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QCOMPARE(resolver.getResolution(" "), "\n");
|
|
QCOMPARE(resolver.getResolution("%"), "%");
|
|
QCOMPARE(resolver.getResolution(" "), " ");
|
|
}
|
|
|
|
void testHexCode()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QCOMPARE(resolver.getResolution(" "), " ");
|
|
QCOMPARE(resolver.getResolution("&"), "&");
|
|
QCOMPARE(resolver.getResolution("+"), "+");
|
|
|
|
}
|
|
|
|
void testMalformedNumericEntity()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
// A numeric entity with no digits ("&#;") leaves an empty value that was
|
|
// previously passed to QString::front() (undefined behaviour). It must
|
|
// now resolve safely rather than crash/assert.
|
|
QCOMPARE(resolver.getResolution("&#;"), "");
|
|
QCOMPARE(resolver.getResolution("&#x;"), "");
|
|
}
|
|
|
|
void testRegisteredEntities()
|
|
{
|
|
TEntityResolver resolver;
|
|
TEntityType type;
|
|
|
|
// Examples from MXP specification https://www.zuggsoft.com/zmud/mxp.htm#ENTITY
|
|
resolver.registerEntity("&Version;", "6.15");
|
|
resolver.registerEntity("&Start;", "<em>");
|
|
resolver.registerEntity("&End;", "</em>");
|
|
|
|
QCOMPARE(resolver.getResolution("&VERSION;"), "6.15");
|
|
QCOMPARE(resolver.getResolution("&Start;"), "<em>");
|
|
QCOMPARE(resolver.getResolution("&end;", true, &type), "</em>");
|
|
QCOMPARE(type, ENTITY_TYPE_CUSTOM);
|
|
// Check if disabling resolution of custom entities works and is properly signalled
|
|
QCOMPARE(resolver.getResolution("&end;", false, &type), "&end;");
|
|
QCOMPARE(type, ENTITY_TYPE_UNKNOWN);
|
|
}
|
|
|
|
void testRegisterEntityAsChar()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
resolver.registerEntity("&symbol;", '@');
|
|
|
|
QCOMPARE(resolver.getResolution("&symbol;"), "@");
|
|
}
|
|
|
|
void testInvalidRegister()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QVERIFY(!resolver.registerEntity("&symbol", '@'));
|
|
QVERIFY(!resolver.registerEntity("symbol", '@'));
|
|
|
|
QVERIFY(resolver.registerEntity("&symbol;", '@'));
|
|
}
|
|
|
|
void testResolveNonExistentEntity()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QCOMPARE(resolver.getResolution("&symbol;"), "&symbol;");
|
|
}
|
|
|
|
void testInterpolation()
|
|
{
|
|
TEntityResolver resolver;
|
|
|
|
QCOMPARE(resolver.interpolate("2 < 4"), "2 < 4");
|
|
QCOMPARE(resolver.interpolate("2 ≪ 4"), "2 < 4");
|
|
QCOMPARE(resolver.interpolate("You say "Hello World""), "You say \"Hello World\"");
|
|
}
|
|
|
|
void testCustomInterpolation()
|
|
{
|
|
const QMap<QString, QString> attributes = {
|
|
{qsl("&name;"), qsl("drunk sailor")},
|
|
{qsl("&desc;"), qsl("A drunk sailor is lying here")}
|
|
};
|
|
|
|
auto mapping = [attributes](auto& attr) {
|
|
auto ptr = attributes.find(attr);
|
|
return ptr != attributes.end() ? *ptr : attr;
|
|
};
|
|
|
|
QCOMPARE(TEntityResolver::interpolate("attack '&name;'|look '&name;'", mapping), "attack 'drunk sailor'|look 'drunk sailor'");
|
|
QCOMPARE(TEntityResolver::interpolate("desc: '&desc;'", mapping), "desc: 'A drunk sailor is lying here'");
|
|
}
|
|
|
|
void cleanupTestCase()
|
|
{
|
|
}
|
|
};
|
|
|
|
#include "TEntityResolverTest.moc"
|
|
QTEST_MAIN(TEntityResolverTest)
|