Skip to content

Do not let a remote URL name a download's directory, or clobber it on… - #15084

Open
nicholasaleks wants to merge 1 commit into
mavlink:masterfrom
nicholasaleks:fix/qgc-02-download-output-path
Open

nicholasaleks wants to merge 1 commit into
mavlink:masterfrom
nicholasaleks:fix/qgc-02-download-output-path

Conversation

@nicholasaleks

Copy link
Copy Markdown

Description

Related to: https://github.com/nicholasaleks/infected-drones/tree/main/QGC-02_component_metadata_uri_ssrf

QGCFileDownload derived the output file name from the remote URL with QUrl::fileName(). That returns the last path segment percent-decoded, and the decode happens before the split on '/', so a '%5C' arrives as a literal '' and is a directory separator on Windows: the write escapes TempLocation entirely. Applying QFileInfo::fileName() reduces it to a plain file name, stripping whatever the host treats as a separator.

The destination was also opened WriteOnly|Truncate before the request was even issued, so a download that 404s, times out, or never reaches a server still destroyed whatever was already at that path. Switching the member to QSaveFile defers that: it writes to a temporary and only replaces the destination on commit(), which now happens once the reply is known good. An uncommitted QSaveFile discards its temporary when destroyed, so the failure paths need nothing beyond the cleanup they already do. QGCFileHelper::atomicWrite() uses the same class for the same reason.

The URLs reaching this code are not trusted: COMPONENT_METADATA.uri, COMPONENT_INFORMATION.general_metadata_uri, the translationUri inside a component-metadata document, and CAMERA_INFORMATION.cam_definition_uri all come straight off an unauthenticated MAVLink link. Two smaller changes follow from that:

  • RequestMetaDataTypeStateMachine::_requestFile() and ComponentInformationTranslation::downloadAndTranslate() now accept only the schemes they actually fetch. QGCFileDownload treats an unrecognised scheme, and any bare string, as a local file path.
  • VehicleCameraControl::_httpRequest() drops QSslSocket::VerifyNone and uses NoLessSafeRedirectPolicy, so a camera-definition URI cannot be silently substituted by an on-path attacker.

Adds two tests: a hostile-name corpus that must resolve inside the download directory, and an existing file that must survive a download that never succeeds. The second fails on the parent commit.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/Build changes
  • Other

Testing

  • Tested locally
  • Added/updated unit tests
  • Tested with simulator (SITL)
  • Tested with hardware

Platforms Tested

  • Linux
  • Windows
  • macOS
  • Android
  • iOS

Flight Stacks Tested

  • PX4
  • ArduPilot

Screenshots

See video in: https://github.com/nicholasaleks/infected-drones/tree/main/QGC-02_component_metadata_uri_ssrf

Checklist

  • I have read the Contribution Guidelines
  • I have read the Code of Conduct
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally

Related Issues

Related to: https://github.com/nicholasaleks/infected-drones/tree/main/QGC-02_component_metadata_uri_ssrf


By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Passed View

All builds passed.

Pre-commit

Check Status Details
pre-commit Failed (non-blocking) View

Pre-commit hooks: 2 passed, 77 failed, 7 skipped.

Test Results

linux-coverage-integration: 54 passed, 0 skipped
linux-coverage-unit: 191 passed, 0 skipped
linux-sanitizers-integration: 31 passed, 0 skipped
linux-sanitizers-unit: 191 passed, 0 skipped
Total: 467 passed, 0 skipped

Code Coverage

Coverage: 71.4%

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl 253.18 MB
QGroundControl-aarch64 177.37 MB
QGroundControl-installer-AMD64 128.44 MB
QGroundControl-installer-AMD64-ARM64 70.83 MB
QGroundControl-installer-ARM64 99.57 MB
QGroundControl-linux 84.77 MB
QGroundControl-mac 84.77 MB
QGroundControl-x86_64 189.50 MB
No baseline available for comparison

Updated: 2026-09-12 03:01:20 UTC • Commit: dfc2ef3 • Triggered by: MacOS

@nicholasaleks
nicholasaleks force-pushed the fix/qgc-02-download-output-path branch from cb7ceb6 to dfc2ef3 Compare September 12, 2026 02:01
@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 11.11111% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.90%. Comparing base (f29efd3) to head (55b044e).
⚠️ Report is 422 commits behind head on master.

Files with missing lines Patch % Lines
src/Utilities/Network/QGCFileDownload.cc 9.09% 4 Missing and 6 partials ⚠️
...nentInformation/RequestMetaDataTypeStateMachine.cc 16.66% 3 Missing and 2 partials ⚠️
src/Camera/VehicleCameraControl.cc 0.00% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (11.11%) is below the target coverage (30.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           master   #15084       +/-   ##
===========================================
+ Coverage   25.47%   38.90%   +13.43%     
===========================================
  Files         769     1087      +318     
  Lines       65912    98413    +32501     
  Branches    30495    44722    +14227     
===========================================
+ Hits        16788    38291    +21503     
+ Misses      37285    36379      -906     
- Partials    11839    23743    +11904     
Flag Coverage Δ
unittests 38.90% <11.11%> (+13.43%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/Utilities/Network/QGCFileDownload.h 26.66% <ø> (-33.34%) ⬇️
src/Camera/VehicleCameraControl.cc 14.35% <0.00%> (+12.59%) ⬆️
...nentInformation/RequestMetaDataTypeStateMachine.cc 48.29% <16.66%> (-1.27%) ⬇️
src/Utilities/Network/QGCFileDownload.cc 27.02% <9.09%> (ø)

... and 841 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cd7132b...55b044e. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… failure

QGCFileDownload derived the output file name from the remote URL with
QUrl::fileName(). That returns the last path segment *percent-decoded*, and the
decode happens before the split on '/', so a '%5C' arrives as a literal '\' and
is a directory separator on Windows: the write escapes TempLocation entirely.
Applying QFileInfo::fileName() reduces it to a plain file name, stripping
whatever the host treats as a separator.

The destination was also opened WriteOnly|Truncate before the request was even
issued, so a download that 404s, times out, or never reaches a server still
destroyed whatever was already at that path. Switching the member to QSaveFile
defers that: it writes to a temporary and only replaces the destination on
commit(), which now happens once the reply is known good. An uncommitted
QSaveFile discards its temporary when destroyed, so the failure paths need
nothing beyond the cleanup they already do. QGCFileHelper::atomicWrite() uses
the same class for the same reason.

The URLs reaching this code are not trusted: COMPONENT_METADATA.uri,
COMPONENT_INFORMATION.general_metadata_uri, the translationUri inside a
component-metadata document, and CAMERA_INFORMATION.cam_definition_uri all come
straight off an unauthenticated MAVLink link. Two smaller changes follow from
that:

  - RequestMetaDataTypeStateMachine::_requestFile() and
    ComponentInformationTranslation::downloadAndTranslate() now accept only the
    schemes they actually fetch. QGCFileDownload treats an unrecognised scheme,
    and any bare string, as a local file path.
  - VehicleCameraControl::_httpRequest() drops QSslSocket::VerifyNone and uses
    NoLessSafeRedirectPolicy, so a camera-definition URI cannot be silently
    substituted by an on-path attacker.

Adds two tests: a hostile-name corpus that must resolve inside the download
directory, and an existing file that must survive a download that never
succeeds. The second fails on the parent commit.
@nicholasaleks
nicholasaleks force-pushed the fix/qgc-02-download-output-path branch from dfc2ef3 to 55b044e Compare September 20, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant