Skip to content

Commit c44a402

Browse files
#1178 Improved AppsServer security: canonical path validation, streaming file responses, and bind() error handling
1 parent 430e850 commit c44a402

2 files changed

Lines changed: 24 additions & 10 deletions

File tree

ui/viewmodel/applications/apps_server.cpp

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ namespace beamui::applications
2121
{
2222
AppsServer::AppsServer(const QString& serveFrom, uint32_t port)
2323
: _documentRoot(serveFrom)
24+
, _canonicalRoot(_documentRoot.canonicalPath())
2425
{
26+
if (_canonicalRoot.isEmpty())
27+
throw std::runtime_error("document root does not exist");
28+
2529
_server = std::make_unique<QHttpServer>();
2630

2731
// Use setMissingHandler as a catch-all to serve static files for any path
@@ -33,27 +37,30 @@ namespace beamui::applications
3337

3438
QString absolutePath = _documentRoot.absoluteFilePath(path);
3539

36-
// Prevent path traversal outside document root
37-
if (!absolutePath.startsWith(_documentRoot.absolutePath())) {
40+
// Resolve symlinks and ".." to a canonical path for safe comparison
41+
QString canonical = QFileInfo(absolutePath).canonicalFilePath();
42+
if (canonical.isEmpty() || !canonical.startsWith(_canonicalRoot)) {
3843
responder.write(QHttpServerResponder::StatusCode::Forbidden);
3944
return;
4045
}
4146

42-
QFileInfo fileInfo(absolutePath);
43-
if (!fileInfo.exists() || fileInfo.isDir()) {
47+
QFileInfo fileInfo(canonical);
48+
if (fileInfo.isDir()) {
4449
responder.write(QHttpServerResponder::StatusCode::NotFound);
4550
return;
4651
}
4752

48-
QFile file(absolutePath);
49-
if (!file.open(QIODevice::ReadOnly)) {
53+
// Stream via QIODevice — avoids loading entire file into memory
54+
auto *file = new QFile(canonical);
55+
if (!file->open(QIODevice::ReadOnly)) {
56+
delete file;
5057
responder.write(QHttpServerResponder::StatusCode::Forbidden);
5158
return;
5259
}
5360

54-
QByteArray content = file.readAll();
55-
QByteArray mimeType = _mimeDb.mimeTypeForFile(absolutePath).name().toUtf8();
56-
responder.write(content, mimeType);
61+
QByteArray mimeType = _mimeDb.mimeTypeForFile(canonical).name().toUtf8();
62+
// Note: This function takes the ownership of data.
63+
responder.write(file, mimeType);
5764
});
5865

5966
_tcpServer = new QTcpServer();
@@ -63,7 +70,13 @@ namespace beamui::applications
6370
_tcpServer = nullptr;
6471
throw std::runtime_error("failed to listen");
6572
}
66-
_server->bind(_tcpServer);
73+
if (!_server->bind(_tcpServer))
74+
{
75+
// listen() succeeded but bind() failed; _tcpServer was not reparented
76+
delete _tcpServer;
77+
_tcpServer = nullptr;
78+
throw std::runtime_error("failed to bind");
79+
}
6780
}
6881

6982
AppsServer::~AppsServer()

ui/viewmodel/applications/apps_server.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace beamui::applications
2929

3030
private:
3131
QDir _documentRoot;
32+
QString _canonicalRoot; // resolved once at construction for path-traversal checks
3233
QMimeDatabase _mimeDb;
3334
std::unique_ptr<QHttpServer> _server;
3435
QTcpServer* _tcpServer = nullptr; // owned by _server after bind()

0 commit comments

Comments
 (0)