[Security] Fix shell injection and temp file leak in qrimage_io#898
[Security] Fix shell injection and temp file leak in qrimage_io#898none34829 wants to merge 2 commits intoSeedSigner:devfrom
Conversation
…in qrimage_io Address both security issues reported in SeedSigner#872: 1. Shell injection: data is now passed to qrencode via stdin instead of being interpolated into a shell command string. The subprocess call uses an explicit argument list with shell=False (the default), so shell metacharacters in QR payloads cannot be interpreted. 2. Temp file leak: /tmp/qrcode.png is now deleted in a finally block immediately after the image is read into memory, preventing sensitive data (PSBTs, xpubs, seed entropy) from persisting on disk. Additionally validates background_color as a 6-character hex string to prevent argument injection via the --background flag. Includes 11 new tests covering the subprocess calling convention, stdin data passing, temp file cleanup, fallback behavior, and input validation.
|
I went through #857 and #872 and ran your test suite locally. The solution feels good to me. I do have a few minor nitpicks though, but mostly very specific edge cases that do not need immediate attention. Would be great from a future standpoint. 1. In line 111 in tmp_path = "/tmp/qrcode.png"Hardcoding It might be safer this way: import tempfile
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
tmp_path = tmp_file.name2. In line 135 in img = Image.open(tmp_path).resize((width, height), Image.Resampling.NEAREST).convert("RGBA")
try:
with Image.open(tmp_path) as original_img:
img = original_img.resize((width, height), Image.Resampling.NEAREST).convert("RGBA")
return img
finally: |
|
Thanks for the review and for running the tests locally. Good points on both. Here's my thinking: Suggestion 2 (context manager on Image.open): Agreed- adopted this in the latest push. The file can hold sensitive data (PSBTs, xpubs), so explicitly closing the handle before unlink makes the intent clearer. On Linux unlink works regardless, but this is better practice. Suggestion 1 (tempfile.NamedTemporaryFile): Valid in general, but SeedSigner runs single-threaded on a Pi Zero- there's no concurrent QR generation scenario. The hardcoded /tmp/qrcode.png is the original code's behavior; this PR just swaps the shell-injection-vulnerable call path around it. I'd rather keep the diff focused on the security fix and not change the tmp file strategy in the same PR. Happy to do it as a follow-up if maintainers want it. |
|
yeah, just gave suggestion 1 to remain foolproof in case of any future multi-threaded plans, not really a necessity as mentioned in my previous comment. Saw it as a diff and thought it was code from your side. But if it was the original behaviour, no issues. |
|
Yeah, the hardcoded path was already there before this PR. Appreciate the review! :) |
Description
Problem or Issue being addressed
Two security issues in
qrimage_io()insrc/seedsigner/helpers/qr.py(reported in #872, related to #857):Shell injection — user-controlled data (QR payloads containing seed entropy, PSBTs, xpubs) is interpolated directly into a shell command string via
subprocess.call(cmd, shell=True). A crafted payload containing shell metacharacters ("; rm -rf / #,$(...), backticks) could execute arbitrary commands.Temp file not deleted —
/tmp/qrcode.pngmay contain sensitive wallet data and persists after use. While SeedSigner OS uses tmpfs, defense-in-depth requires cleanup.Solution
Shell injection fix:
subprocess.call(cmd, shell=True)withsubprocess.run([...], input=data_bytes, capture_output=True)/proc/*/cmdline)Temp file cleanup:
Image.open()call in atry/finallyblock that unconditionally callsos.unlink()on the temp fileImage.open()or the resize/convert chain raises an exceptionInput validation:
background_colorto ensure it is a 6-character hex string, preventing arbitrary argument injection via the--backgroundflagNote on existing PR #862: That PR addresses #857 (shell injection only) by switching to an argument list, but passes data as a command-line argument rather than via stdin. This PR takes the approach recommended in #872 — stdin — which also avoids process-list exposure and argument-length limits. This PR additionally addresses the temp file leak and adds input validation, which #862 does not cover.
Additional Information
qrencodereturns a non-zero exit code,qrimage_iofalls back to the pure-Pythonqrimageencoder.import ostoqr.py(previously not imported).Screenshots
N/A — no UI changes. This is a security hardening of internal subprocess handling.
This pull request is categorized as a:
Checklist
I ran
pytestlocallyI included screenshots of any new or modified screens
Should be part of the PR description above.
I added or updated tests
Any new or altered functionality should be covered in a unit test. Any new or updated sequences require FlowTests.
11 new tests in
tests/test_qr_security.pycovering:shell=True)Image.open()failureqrencodefailurebackground_colorbackground_colorvalues replaced with defaultcapture_output=Trueis set to suppress stderr leakageI tested this PR hands-on on the following platform(s):
Tested via mocked unit tests on Windows. The
qrencodebinary is a Linux/Pi dependency; all behavioral contracts are verified through mocks. Happy to test on Pi hardware if requested.I have reviewed these notes:
Thank you! Please join our Devs' Telegram group to get more involved.
Fixes #872
Also resolves #857