Skip to content

fix: validate PreUninstall script path to prevent CWE-78 command injection - #833

Open
mhduiy wants to merge 1 commit into
masterfrom
agent/developer/c095abe9c85a
Open

mhduiy wants to merge 1 commit into
masterfrom
agent/developer/c095abe9c85a

Conversation

@mhduiy

@mhduiy mhduiy commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

修复说明

在 AppWiz::legacyRequestUninstall() 执行 X-Deepin-PreUninstall 脚本前新增 isPreUninstallScriptTrusted() 校验,修复 CWE-78 命令注入风险。

防护措施(三层)

  • 绝对路径检查:要求脚本路径为绝对路径,防止 PATH 劫持
  • 可信目录白名单:仅允许 /usr/share/、/usr/lib/、/usr/libexec/、/opt/ 下的脚本
  • 文件属主与权限校验:验证文件非符号链接、属主为 root、不可被 group/other 写入

行为变更

  • 不可信脚本跳过执行并记录日志,但继续 DBus 卸载流程,不影响正常卸载功能

关联 Issue

DDE-520

Summary by Sourcery

Secure legacy application uninstallation by validating pre-uninstall scripts before execution.

Bug Fixes:

  • Prevent command injection through untrusted pre-uninstall script paths by validating their location, file type, ownership, and permissions before execution.

Enhancements:

  • Continue the DBus uninstall flow while logging and skipping pre-uninstall scripts that fail trust validation.

@deepin-ci-robot

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: mhduiy

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

@sourcery-ai

sourcery-ai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR mitigates CWE-78 by validating pre-uninstall script paths and file security attributes before direct QProcess execution, while allowing the uninstall DBus flow to continue when a script is untrusted and preserving existing script failure semantics.

Sequence diagram for trusted pre-uninstall script execution

sequenceDiagram
    participant AppWiz
    participant Validator as isPreUninstallScriptTrusted
    participant FileSystem as QFileInfo
    participant Script as PreUninstallScript
    participant DBus as DBusUninstallFlow

    AppWiz->>Validator: isPreUninstallScriptTrusted(args[0])
    Validator->>FileSystem: isSymLink()
    Validator->>FileSystem: isFile()
    Validator->>FileSystem: ownerId()
    Validator->>FileSystem: permissions()
    alt trust validation passes
        Validator-->>AppWiz: true
        AppWiz->>Script: QProcess::start(args[0], args.mid(1))
        AppWiz->>Script: waitForFinished(-1)
        Script-->>AppWiz: exit status
        AppWiz->>DBus: continue uninstall flow
    else trust validation fails
        Validator-->>AppWiz: false
        AppWiz->>DBus: skip script and continue uninstall flow
    end
Loading

Flow diagram for pre-uninstall script trust validation

flowchart TD
    A[Pre-uninstall script path] --> B[QProcess::splitCommand]
    B --> C{Valid command}
    C -->|No| X[Abort uninstall]
    C -->|Yes| D[isPreUninstallScriptTrusted]
    D --> E{Absolute path}
    E -->|No| R[Reject and log]
    E -->|Yes| F{Trusted directory}
    F -->|No| R
    F -->|Yes| G{Regular root-owned non-writable file}
    G -->|No| R
    G -->|Yes| H[QProcess::start]
    R --> I[Skip script]
    H --> J{Script exit status}
    J -->|101 or 103| X
    J -->|Other failure| K[Log failure]
    J -->|Success| L[Continue uninstall]
    K --> L
    I --> L
Loading

File-Level Changes

Change Details Files
Add layered trust validation before executing a pre-uninstall script.
  • Require an absolute path under the approved system-directory prefixes.
  • Reject symlinks and non-regular files.
  • Require root ownership and no group/other write permissions.
  • Log the rejection reason and skip execution when validation fails.
src/ddeintegration/appwiz.cpp
Preserve the existing uninstall control flow while integrating validation.
  • Continue the DBus uninstall flow for scripts that fail trust validation.
  • Retain argument parsing and existing handling of script exit codes 101, 103, and other failures.
  • Continue launching validated scripts directly via QProcess without a shell.
src/ddeintegration/appwiz.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

@mhduiy
mhduiy marked this pull request as ready for review October 30, 2026 06:04

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/ddeintegration/appwiz.cpp" line_range="48-61" />
<code_context>
+    }
+
+    // Layer 3: validate file type, ownership and permissions.
+    QFileInfo fi(scriptPath);
+    if (fi.isSymLink()) {
+        qCWarning(logDdeIntegration) << "Pre-uninstall script is a symbolic link, rejected:" << scriptPath;
+        return false;
+    }
+    if (!fi.isFile()) {
+        qCWarning(logDdeIntegration) << "Pre-uninstall script is not a regular file, rejected:" << scriptPath;
+        return false;
+    }
+    if (fi.ownerId() != 0) {
+        qCWarning(logDdeIntegration) << "Pre-uninstall script is not owned by root, rejected:" << scriptPath;
+        return false;
+    }
+    if (fi.permissions() & 0o022) {
+        qCWarning(logDdeIntegration) << "Pre-uninstall script is writable by group or others, rejected:" << scriptPath;
+        return false;
</code_context>
<issue_to_address>
**Replacement script bypasses validation**

When an attacker can modify the script or an ancestor directory and replaces the path after validation but before process start, `QProcess::start()` resolves the path again and executes the replacement without the ownership or permission checks performed by `isPreUninstallScriptTrusted()`.

Ensure the process executes the same file that was validated, or otherwise prevent the validated path and its ancestors from being replaced before launch.

Also at `src/ddeintegration/appwiz.cpp:115-119`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/ddeintegration/appwiz.cpp Outdated
Comment on lines +48 to +61
QFileInfo fi(scriptPath);
if (fi.isSymLink()) {
qCWarning(logDdeIntegration) << "Pre-uninstall script is a symbolic link, rejected:" << scriptPath;
return false;
}
if (!fi.isFile()) {
qCWarning(logDdeIntegration) << "Pre-uninstall script is not a regular file, rejected:" << scriptPath;
return false;
}
if (fi.ownerId() != 0) {
qCWarning(logDdeIntegration) << "Pre-uninstall script is not owned by root, rejected:" << scriptPath;
return false;
}
if (fi.permissions() & 0o022) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Replacement script bypasses validation

When an attacker can modify the script or an ancestor directory and replaces the path after validation but before process start, QProcess::start() resolves the path again and executes the replacement without the ownership or permission checks performed by isPreUninstallScriptTrusted().

Ensure the process executes the same file that was validated, or otherwise prevent the validated path and its ancestors from being replaced before launch.

Also at src/ddeintegration/appwiz.cpp:115-119.

Prompt for AI agents
In `src/ddeintegration/appwiz.cpp` at lines 48-61:

**Replacement script bypasses validation**

When an attacker can modify the script or an ancestor directory and replaces the path after validation but before process start, `QProcess::start()` resolves the path again and executes the replacement without the ownership or permission checks performed by `isPreUninstallScriptTrusted()`.

Ensure the process executes the same file that was validated, or otherwise prevent the validated path and its ancestors from being replaced before launch.

Also at `src/ddeintegration/appwiz.cpp:115-119`.

…ction

Add isPreUninstallScriptTrusted() to validate the X-Deepin-PreUninstall
script path before execution:
- Require absolute path (prevent PATH hijacking)
- Whitelist trusted system directories (/usr/share/, /usr/lib/,
  /usr/libexec/, /opt/)
- Verify file is not a symlink, is owned by root, and is not
  group/other-writable

Untrusted scripts are skipped (logged) while the DBus uninstall flow
continues normally.
@mhduiy
mhduiy force-pushed the agent/developer/c095abe9c85a branch from dadc830 to a3423dd Compare October 30, 2026 16:52
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

🤖 AI 代码审查报告

总体评分: 40 分 (通过阈值: 70分)

Fail


📊 总体评价

项目 结果
审查结论 代码审查未通过
评分详情 本 PR 旨在修复 CWE-78 命令注入漏洞,通过新增 isPreUninstallScriptTrusted 函数对 PreUninstall 脚本路径进行多层校验(绝对路径、可信目录白名单、文件类型/属主/权限检查),整体设计方向正确、注释完善。 但安全维度存在路径穿越漏洞:QString::startsWith() 对未规范化的原始路径进行前缀匹配,攻击者可通过 ".." 组件(如 /usr/share/../../usr/bin/rm)绕过可信目录白名单,执行白名单外的 root 属主可执行文件, 导致安全修复不完整。同时 isSymLink() 仅检查末级路径组件,无法检测父目录中的符号链接。建议使用 QFileInfo::canonicalFilePath() 规范化路径后再做前缀校验,可同时修复以上两个问题。

🔍 详细分析

1. 语法逻辑 ✅

评价: 语法正确,逻辑清晰 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: ['无需修改,语法逻辑正确。']


2. 代码质量 ✅

评价: 代码结构清晰,注释完整 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: ['权限掩码 022 建议增加行内注释说明含义(group-write | other-write),提升可读性。', '调用处信任校验失败时建议补充 qCWarning 日志,与函数内部的审计日志保持一致。']


3. 代码性能 ✅

评价: 性能良好,资源使用合理 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: ['无需修改,性能良好。']


4. 代码安全 🔓

评价: 存在 3 个安全漏洞 ❌ 不通过

🔐 发现 3 个安全漏洞

安全漏洞详情:

  1. 🟡 路径穿越 (CWE-22) (未知文件 - 未知函数): QString::startsWith() 对未规范化的原始路径进行可信目录前缀匹配,攻击者可通过 ".." 组件 构造如 /usr/share/../../usr/bin/rm 的路径绕过白名单。该路径以 /usr/share/ 开头通过前缀检查, 但实际解析至 /usr/bin/rm(root 属主的常规文件,通过属主和权限检查),配合桌面文件中可控的 参数即可执行任意 root 属主可执行文件,导致 CWE-78 命令注入修复不完整——非常重要
  2. 🟡 符号链接跟随 (CWE-59) (未知文件 - 未知函数): QFileInfo::isSymLink() 仅检查路径末级组件是否为符号链接,无法检测父目录中的符号链接。 若可信目录内存在父目录符号链接(如 /usr/share/myapp -> /tmp/evil/),则路径 /usr/share/myapp/script.sh 可通过所有检查但实际指向不可信位置。使用 canonicalFilePath() 可同时解决此问题——非常重要
  3. 🟡 安全审计日志缺失 (未知文件 - 未知函数): 调用处信任校验失败时仅有代码注释,无 qCWarning 日志。若生产环境未启用 logDdeIntegration 警告类别,函数内部的拒绝日志不可见,安全关键的脚本跳过事件将无法追踪——非常重要

建议: ['在 Layer 2 前使用 QFileInfo::canonicalFilePath() 规范化路径,再用规范化后的路径做前缀校验和后续文件属性检查,可同时修复路径穿越和父目录符号链接两个问题', '调用处增加 qCWarning 日志记录脚本被跳过的事件,用于安全审计']


💡 改进建议代码示例

#include <QFileInfo>
#include <QDir>

static bool isPreUninstallScriptTrusted(const QString &scriptPath)
{
    // Layer 1: must be an absolute path to prevent PATH hijacking.
    if (!scriptPath.startsWith(QLatin1Char('/'))) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script path is not absolute, rejected:" << scriptPath;
        return false;
    }

    // Canonicalize the path first to resolve any `..` components or symlinks
    // (including those in parent directories) that could bypass the prefix check.
    const QString canonicalPath = QFileInfo(scriptPath).canonicalFilePath();
    if (canonicalPath.isEmpty()) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script path does not exist or cannot be resolved, rejected:" << scriptPath;
        return false;
    }

    // Layer 2: must reside within a trusted system directory (check against canonical path).
    static constexpr const char *trustedPrefixes[] = {
        "/usr/share/",
        "/usr/lib/",
        "/usr/libexec/",
        "/opt/",
    };
    bool inTrustedDir = false;
    for (const char *prefix : trustedPrefixes) {
        if (canonicalPath.startsWith(QLatin1String(prefix))) {
            inTrustedDir = true;
            break;
        }
    }
    if (!inTrustedDir) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script path is not in a trusted directory, rejected:" << canonicalPath;
        return false;
    }

    // Layer 3: validate file type, ownership and permissions (using canonical path).
    QFileInfo fi(canonicalPath);
    // canonicalFilePath() resolves all symlinks, so fi.isSymLink() is always false here.
    // Keep the check as redundant defense-in-depth.
    if (fi.isSymLink()) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script is a symbolic link, rejected:" << canonicalPath;
        return false;
    }
    if (!fi.isFile()) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script is not a regular file, rejected:" << canonicalPath;
        return false;
    }
    if (fi.ownerId() != 0) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script is not owned by root, rejected:" << canonicalPath;
        return false;
    }
    // Reject if writable by group (020) or others (002).
    if (fi.permissions() & 022) {
        qCWarning(logDdeIntegration) << "Pre-uninstall script is writable by group or others, rejected:" << canonicalPath;
        return false;
    }

    return true;
}

// --- At the call site ---
if (!isPreUninstallScriptTrusted(args[0])) {
    qCWarning(logDdeIntegration) << "Pre-uninstall script failed trust validation, skipping execution for:" << desktopFileFullPath;
    // skip script execution and continue with the DBus uninstall flow below.
} else {
    // ... existing QProcess execution logic ...
}

本报告由 AI 代码审查工具自动生成

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.

2 participants