Skip to content

Conversation

@LiHua000
Copy link
Contributor

@LiHua000 LiHua000 commented Dec 19, 2025

  • Replace single-pass "../" removal with loop to remove all occurrences
  • Add final path validation to ensure extracted files stay within target directory

Log: fix CITIVD

Bug: https://pms.uniontech.com/bug-view-342883.html

Summary by Sourcery

Harden zip entry extraction to prevent path traversal outside the target directory.

Bug Fixes:

  • Strip all "../" components from zip entry names instead of only the first occurrence to close a path traversal vulnerability.
  • Validate the final extraction path and reject files whose resolved path would escape the configured target directory.

- Replace single-pass "../" removal with loop to remove all occurrences
- Add final path validation to ensure extracted files stay within target directory

Log: fix CITIVD

Bug: https://pms.uniontech.com/bug-view-342883.html
@deepin-ci-robot
Copy link

deepin pr auto review

我来对这段代码的修改进行审查:

  1. 安全性改进:
  • 原代码只检查了一次"../"的存在,现在改为循环检查,这是一个很好的改进,可以防止多层目录遍历攻击(如"../../..")。
  • 新增了路径规范化检查,使用QDir::cleanPath()和absolutePath()来确保最终路径在目标目录内,这是另一个重要的安全改进。
  1. 代码质量改进:
  • 注释更加清晰,明确指出了修复的目的是防止路径遍历攻击。
  • 代码逻辑更加健壮,使用了更安全的路径处理方式。
  1. 建议进一步改进:
  • 可以考虑使用QDir::cleanPath()来处理strFileName,而不是简单地替换"../",因为cleanPath()会处理更多边界情况。
  • 建议在路径检查时也考虑符号链接的情况,因为符号链接也可能导致路径遍历攻击。
  1. 性能考虑:
  • 当前实现中的循环替换"../"可能会有性能问题,特别是当路径中包含大量"../"时。建议使用更高效的路径处理方法。
  1. 其他建议:
  • 可以考虑将路径安全检查提取为单独的函数,以提高代码的可维护性和可测试性。
  • 建议在日志中记录更多信息,如原始路径和处理后的路径,以便于调试和审计。

改进后的代码示例:

// 安全处理文件路径
QString sanitizePath(const QString& path) {
    QString cleanPath = QDir::cleanPath(path);
    // 移除开头的斜杠
    while (cleanPath.startsWith('/') || cleanPath.startsWith('\\')) {
        cleanPath = cleanPath.mid(1);
    }
    return cleanPath;
}

// 在extractEntry函数中使用
strFileName = sanitizePath(m_common->trans2uft8(statBuffer.name, m_mapFileCode[index]));

// 验证路径是否在目标目录内
QString cleanTargetPath = QDir::cleanPath(options.strTargetPath);
QString cleanDestPath = QDir::cleanPath(options.strTargetPath + "/" + strFileName);
if (!cleanDestPath.startsWith(cleanTargetPath)) {
    qInfo() << "Path traversal detected! Original path:" << statBuffer.name 
            << "Sanitized path:" << strFileName;
    return ET_FileWriteError;
}

这些改进可以:

  1. 提供更安全的路径处理
  2. 提高代码的可维护性
  3. 提供更好的错误日志记录
  4. 处理更多的边界情况
  5. 使用更高效的路径处理方法

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 19, 2025

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Strengthens zip extraction security by fully stripping "../" components from entry names and adding a final canonical-path containment check to ensure extracted files cannot escape the configured target directory.

Sequence diagram for secured zip entry extraction

sequenceDiagram
    actor User
    participant Application
    participant LibzipPlugin
    participant QDir
    participant QFileSystem

    User ->> Application: Request archive extraction
    Application ->> LibzipPlugin: extractEntry(archive, index, options)

    LibzipPlugin ->> LibzipPlugin: trans2uft8(statBuffer.name, m_mapFileCode[index])
    LibzipPlugin ->> LibzipPlugin: strFileName assigned

    loop Remove all ../ components
        LibzipPlugin ->> LibzipPlugin: while strFileName.contains("../")
        LibzipPlugin ->> LibzipPlugin: replace("../", "")
        LibzipPlugin ->> LibzipPlugin: log skipped ../ path components
    end

    LibzipPlugin ->> LibzipPlugin: strDestFileName = options.strTargetPath + separator + strFileName

    LibzipPlugin ->> QDir: cleanTargetPath = cleanPath(absolutePath(options.strTargetPath))
    LibzipPlugin ->> QDir: cleanDestPath = cleanPath(absolutePath(strDestFileName))

    LibzipPlugin ->> LibzipPlugin: check containment
    alt Path escapes target directory
        LibzipPlugin ->> LibzipPlugin: log path traversal detected
        LibzipPlugin -->> Application: return ET_FileWriteError
        Application -->> User: Report extraction failure
    else Path within target directory
        LibzipPlugin ->> QFileSystem: QFile file(strDestFileName)
        LibzipPlugin ->> QFileSystem: Write extracted data
        LibzipPlugin -->> Application: return success
        Application -->> User: Report extraction success
    end
Loading

Flow diagram for zip entry path sanitization and validation

flowchart TD
    A[Start extractEntry] --> B[Compute strFileName from archive entry]
    B --> C{strFileName contains ../ ?}
    C -- Yes --> D[Log skipped ../ path components]
    D --> E[Remove ../ from strFileName]
    E --> C
    C -- No --> F[Build strDestFileName = targetPath + separator + strFileName]
    F --> G["cleanTargetPath = cleanPath(absolutePath(targetPath))"]
    G --> H["cleanDestPath = cleanPath(absolutePath(strDestFileName))"]
    H --> I{cleanDestPath startsWith cleanTargetPath + separator<br/>or cleanDestPath equals cleanTargetPath ?}
    I -- No --> J[Log path traversal detected]
    J --> K[Return ET_FileWriteError]
    I -- Yes --> L[Create QFile for strDestFileName]
    L --> M[Write extracted data]
    M --> N[Return success]
    K --> O[End]
    N --> O[End]
Loading

File-Level Changes

Change Details Files
Harden path normalization to remove all parent-directory traversal tokens from zip entry names.
  • Replace single index-based check for "../" with a while-loop that repeatedly removes all "../" substrings from the extracted entry name.
  • Log a message whenever any "../" components are skipped during normalization.
3rdparty/libzipplugin/libzipplugin.cpp
Add a canonical path containment check before writing extracted files to disk.
  • Construct absolute, cleaned versions of the target directory path and the destination file path using QDir::cleanPath and absolutePath.
  • Reject extraction with ET_FileWriteError and log a message if the computed destination path is not within the cleaned target directory (protecting against residual path traversal attempts).
3rdparty/libzipplugin/libzipplugin.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The loop that repeatedly replaces "../" in strFileName only covers this exact Unix-style pattern and can be bypassed (e.g., ..\, mixed slashes, or encoded forms); consider normalizing the entry path and validating components (e.g., via QDir::cleanPath and rejecting any .. segments) instead of string replacement.
  • The startsWith(cleanTargetPath + QDir::separator()) check may not behave as expected on case-insensitive filesystems or if cleanTargetPath already has a trailing separator; it would be safer to normalize both paths and compare directory components or use QDir::isRelativePath/canonical paths for containment checks.
  • Using ET_FileWriteError for a rejected path traversal attempt blends security validation with IO failures; consider introducing or reusing a more specific error code to distinguish security violations from write errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The loop that repeatedly replaces "../" in `strFileName` only covers this exact Unix-style pattern and can be bypassed (e.g., `..\`, mixed slashes, or encoded forms); consider normalizing the entry path and validating components (e.g., via `QDir::cleanPath` and rejecting any `..` segments) instead of string replacement.
- The `startsWith(cleanTargetPath + QDir::separator())` check may not behave as expected on case-insensitive filesystems or if `cleanTargetPath` already has a trailing separator; it would be safer to normalize both paths and compare directory components or use `QDir::isRelativePath`/canonical paths for containment checks.
- Using `ET_FileWriteError` for a rejected path traversal attempt blends security validation with IO failures; consider introducing or reusing a more specific error code to distinguish security violations from write errors.

## Individual Comments

### Comment 1
<location> `3rdparty/libzipplugin/libzipplugin.cpp:800-802` </location>
<code_context>
-    //fix 232873
-    if(strFileName.indexOf("../") != -1) {
+    //fix 232873 - Remove all "../" components to prevent path traversal attacks
+    while(strFileName.contains("../")) {
         qInfo() << "skipped ../ path component(s) in " << strFileName;
         strFileName = strFileName.replace("../", "");
     }
</code_context>

<issue_to_address>
**issue (bug_risk):** Stripping all "../" components can result in an empty filename and unexpected extraction target.

If the entry name is "../" or all components are stripped, `strFileName` becomes empty and `strDestFileName` ends up as just the target directory. The extraction then operates on a directory path instead of a file path. Please add an explicit check for an empty (or "."/"/") `strFileName` and reject such entries before proceeding.
</issue_to_address>

### Comment 2
<location> `3rdparty/libzipplugin/libzipplugin.cpp:895-907` </location>
<code_context>
     QString strDestFileName = options.strTargetPath + QDir::separator() + strFileName;

+    // Additional security check: ensure the final path is within the target directory
+    QString cleanTargetPath = QDir::cleanPath(QDir(options.strTargetPath).absolutePath());
+    QString cleanDestPath = QDir::cleanPath(QDir(strDestFileName).absolutePath());
+    if (!cleanDestPath.startsWith(cleanTargetPath + QDir::separator()) &&
+        cleanDestPath != cleanTargetPath) {
+        qInfo() << "Path traversal detected! Rejected path: " << strFileName;
</code_context>

<issue_to_address>
**🚨 suggestion (security):** The path containment check should consider platform-specific case sensitivity and use path semantics, not plain string comparison.

The `startsWith` check adds protection but is still fragile:
- On case‑insensitive filesystems (Windows, default macOS), a case‑sensitive string comparison can be bypassed with different casing.
- Pure string prefix checks ignore filesystem path semantics (components, roots, symlinks).

Where possible, compare canonicalized paths via QDir/QFileInfo (e.g. `canonicalPath()`), and normalize case on case‑insensitive platforms before applying the containment check. This will make the traversal safeguard more robust.

```suggestion
    // 解压完整文件名(含路径)
    QString strDestFileName = options.strTargetPath + QDir::separator() + strFileName;

    // Additional security check: ensure the final path is within the target directory.
    // Use canonical/absolute paths and platform-specific case rules instead of raw string prefix.
    QFileInfo targetInfo(options.strTargetPath);
    QString cleanTargetPath = targetInfo.canonicalFilePath();
    if (cleanTargetPath.isEmpty()) {
        cleanTargetPath = QDir::cleanPath(targetInfo.absoluteFilePath());
    }

    QFileInfo destInfo(strDestFileName);
    QString cleanDestPath;

    // Try to canonicalize as much of the destination path as possible (may not exist yet).
    QFileInfo destDirInfo(destInfo.path());
    QString canonicalDestDir = destDirInfo.canonicalFilePath();
    if (!canonicalDestDir.isEmpty()) {
        cleanDestPath = QDir::cleanPath(canonicalDestDir + QDir::separator() + destInfo.fileName());
    } else {
        cleanDestPath = QDir::cleanPath(destInfo.absoluteFilePath());
    }

    // Normalize for case-insensitive platforms so that casing differences cannot bypass the check.
#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN)
    QString normTargetPath = cleanTargetPath.toLower();
    QString normDestPath = cleanDestPath.toLower();
#else
    const QString &normTargetPath = cleanTargetPath;
    const QString &normDestPath = cleanDestPath;
#endif

    // Compare using path semantics (components) instead of plain string prefix.
    QStringList targetComponents = normTargetPath.split(QChar('/'), Qt::SkipEmptyParts);
    QStringList destComponents = normDestPath.split(QChar('/'), Qt::SkipEmptyParts);

    bool isContained = true;
    if (targetComponents.size() > destComponents.size()) {
        isContained = false;
    } else {
        for (int i = 0; i < targetComponents.size(); ++i) {
            if (targetComponents.at(i) != destComponents.at(i)) {
                isContained = false;
                break;
            }
        }
    }

    if (!isContained) {
        qInfo() << "Path traversal detected! Rejected path:" << strFileName
                << "resolved to:" << cleanDestPath << "outside of:" << cleanTargetPath;
        return ET_FileWriteError;
    }

    QFile file(strDestFileName);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: LiHua000, lzwind

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@LiHua000
Copy link
Contributor Author

/merge

@deepin-bot deepin-bot bot merged commit 1fa62df into linuxdeepin:develop/snipe Dec 19, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants