Skip to content

Fix shell injection risk via malicious QR code URL passed to adb#4

Merged
jackfreem merged 5 commits intoblock:mainfrom
jackfreem:jf/SanitizeURLs
Mar 12, 2026
Merged

Fix shell injection risk via malicious QR code URL passed to adb#4
jackfreem merged 5 commits intoblock:mainfrom
jackfreem:jf/SanitizeURLs

Conversation

@jackfreem
Copy link
Collaborator

@jackfreem jackfreem commented Mar 10, 2026

Closes #3

Why

URLs from scanned QR codes were passed unsanitized to adb shell am start -d <url>, allowing a malicious QR code containing shell metacharacters (e.g. ;, |, $()) to execute arbitrary commands on the connected Android device.

What

  • Add sanitizeUrlForAndroidShell() to Helpers/URLSanitizer.swift; validates URL scheme against an allowlist (http, https, cashme) and rejects shell meta characters after URL re-serialization
  • Wire validation into AndroidEmulatorHelper.openUrl via a validateAndSanitizeUrl helper before the URL is passed to adb
  • Extract allowedUrlSchemas as a shared constant so the sanitizer and error-reporting logic share a single source of truth.

References

Tests

Screenshot 2026-03-10 at 3 27 49 PM Screenshot 2026-03-10 at 3 27 29 PM

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to mitigate Android shell injection when opening URLs (e.g., from scanned QR codes) by validating/sanitizing the URL before passing it into adb shell am start -d.

Changes:

  • Added a URL sanitizer helper intended to reject malformed/unsupported schemes and shell metacharacters.
  • Wired URL sanitization into AndroidEmulatorHelper.openUrl so only a validated URL is passed to adb.
  • Added user-facing error messages describing why a URL was rejected.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
Sources/qrgo/Helpers/URLSanitizer.swift Introduces sanitizeUrlForAndroidShell to validate scheme and reject dangerous characters.
Sources/qrgo/Helpers/AndroidEmulatorHelper.swift Adds validation wrapper and uses the sanitized URL in the adb shell am start invocation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

… allowlist

- Replace broad shell-metacharacter denylist with single-quote approach:
  sanitizeUrlForAndroidShell now only rejects characters that can break
  out of POSIX single quotes (', \0, \r, \n), allowing legitimate URLs
  with & in query strings to pass through safely
- Wrap the sanitized URL in single quotes in the adb shell invocation
  so the Android /bin/sh treats & ; | etc. as literal URL data
- Extract allowedUrlSchemes constant so sanitizeUrlForAndroidShell and
  validateAndSanitizeUrl share a single source of truth for the allowlist
- Update PR description to reflect actual package structure (no qrgoLib
  library target; sanitizer lives alongside other helpers in qrgo target)
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Pass am start as a single command string to adb shell so the Android
  device's /bin/sh interprets the single-quote wrapping around the URL,
  rather than relying on adb to preserve embedded quotes in argv
- Clarify URLSanitizer doc: the function does not reject shell
  metacharacters like & ; | — it relies on the caller single-quoting
  the URL in the command string
@jackfreem jackfreem marked this pull request as ready for review March 12, 2026 14:15
afollestad
afollestad approved these changes Mar 12, 2026
printError("ADB not found. Please install Android SDK or ensure ADB is in your PATH.")
return false
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

P1: The old code passed arguments as separate array elements to adb. The new code concatenates everything into a single shell command string: "am start ... -d '\(safeUrlString)'". While the single-quoting + sanitizer approach is correct, the security now depends entirely on sanitizeUrlForAndroidShell never having a bug. Could this keep the separate-argument style and add shell-escaping just for the URL argument? Or at minimum, could a comment be added here explaining the security invariant — that the sanitizer guarantees no single-quote breakout, and that this string must always be constructed with single quotes around the URL?

@@ -107,13 +107,34 @@ class AndroidEmulatorHelper {
return props
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2: When sanitizeUrlForAndroidShell returns nil, this method re-parses the URL with URL(string:) to determine which error to show. Would it be cleaner for the sanitizer to return an error enum (e.g., .malformed, .badScheme(String), .dangerousCharacters) instead of a bare nil? That would avoid duplicated parsing and keep error messages in sync with rejection reasons.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call - switched.

import Foundation

/// Allowed URL schemes for opening on Android devices.
let allowedUrlSchemes: Set<String> = ["http", "https", "cashme"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2: allowedUrlSchemes is a module-level let with internal access. Since it's shared between URLSanitizer.swift and AndroidEmulatorHelper.swift this works, but is it intentional that any file in the module can read this set? If this should be the canonical allowlist, consider documenting that intent or noting it's the single source of truth for scheme validation.

printError("ADB not found. Please install Android SDK or ensure ADB is in your PATH.")
return false
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: Worth a brief inline comment noting that when adb shell receives a single argument, it passes it to sh -c on the device — which is why single-quoting the URL is the correct mitigation. This context would help future maintainers understand the security model.

- Return Result<String, URLSanitizationError> from sanitizeUrlForAndroidShell
  instead of String? so callers get a typed reason for rejection (.malformed,
  .disallowedScheme, .dangerousCharacters) without re-parsing the URL
- Update validateAndSanitizeUrl to switch on the error cases directly,
  removing the duplicated URL(string:) re-parse for error message selection
- Document allowedUrlSchemes as the single source of truth for scheme
  validation across the module
- Add inline comment above the adb shell invocation explaining the security
  model: adb shell passes the string to /bin/sh -c on device, single quotes
  neutralize metacharacters, and the sanitizer ensures no single-quote breakout
@jackfreem jackfreem merged commit 3708d4d into block:main Mar 12, 2026
3 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.

Shell injection risk via malicious QR code URL passed to adb

3 participants