Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/linglong/src/linglong/cli/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1715,7 +1715,7 @@ int Cli::content(const ContentOptions &options)

QDirIterator it(entriesDir.absolutePath(),
QDir::AllEntries | QDir::NoDot | QDir::NoDotDot | QDir::System,
QDirIterator::Subdirectories);
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Enabling QDirIterator::FollowSymlinks allows the iterator to descend into symlinked directories. This introduces a potential security risk if a package contains malicious symlinks pointing to sensitive host files (e.g., /etc/shadow).

When FollowSymlinks is active, it.fileInfo().absoluteFilePath() returns the path of the symlink target. If that target is outside the entriesDir, entriesDir.relativeFilePath(entryPath) will produce a path starting with ... It is highly recommended to validate that the resolved entryPath is still within the package's layer directory to prevent path traversal vulnerabilities during the export process.

while (it.hasNext()) {
it.next();
const auto entryPath = it.fileInfo().absoluteFilePath();
Expand Down
84 changes: 84 additions & 0 deletions libs/linglong/tests/ll-tests/src/linglong/cli/cli_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,90 @@ TEST_F(CliTest, contentPreferDesktopFromDefaultSharedDir)
EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentResolvesDesktopFromSymlinkedEntriesShareDir)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-symlinked-share";
const auto layerFilesDir =
tempDir->path() / "layers" / commit / "files" / "share" / "applications";
const auto layerEntriesShareLink = tempDir->path() / "layers" / commit / "entries" / "share";
const auto defaultDesktopPath =
repo->defaultSharedDir() / "applications" / "org.example.app.desktop";

std::filesystem::create_directories(layerFilesDir);
std::filesystem::create_directories(layerEntriesShareLink.parent_path());
std::filesystem::create_directories(defaultDesktopPath.parent_path());
std::ofstream(layerFilesDir / "org.example.app.desktop") << "Test desktop content";
std::ofstream(defaultDesktopPath) << "Test desktop content in default";

std::error_code ec;
std::filesystem::create_directory_symlink("../files/share", layerEntriesShareLink, ec);
if (ec) {
GTEST_SKIP() << "directory symlink support is required: " << ec.message();
}

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer,
printContent(ElementsAre(QString::fromStdString(defaultDesktopPath.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentResolvesOverlayDesktopFromSymlinkedEntriesShareDir)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-symlinked-share-overlay";
const auto layerFilesDir =
tempDir->path() / "layers" / commit / "files" / "share" / "applications";
const auto layerEntriesShareLink = tempDir->path() / "layers" / commit / "entries" / "share";
const auto overlayDesktopPath =
repo->overlaySharedDir() / "applications" / "org.example.app.desktop";

std::filesystem::create_directories(layerFilesDir);
std::filesystem::create_directories(layerEntriesShareLink.parent_path());
std::filesystem::create_directories(overlayDesktopPath.parent_path());
std::ofstream(layerFilesDir / "org.example.app.desktop") << "Test desktop content";
std::ofstream(overlayDesktopPath) << "Test desktop content in overlay";

std::error_code ec;
std::filesystem::create_directory_symlink("../files/share", layerEntriesShareLink, ec);
if (ec) {
GTEST_SKIP() << "directory symlink support is required: " << ec.message();
}

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer,
printContent(ElementsAre(QString::fromStdString(overlayDesktopPath.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentFallbackDesktopToOverlaySharedDir)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
Expand Down
Loading