Skip to content

Releases: steck0714/Mock-webusb

v0.0.3b

v0.0.3b Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 03 Aug 00:05
9dd8eea

Summary of Implementation

  • frame_origin.py (New Module) — FrameOriginTracker
    • The Python side (the completely zero-trust side that does not trust JS at all) traverses the frame tree using page.mainFrame() / QWebEngineFrame.children().
    • It distributes an unguessable token to each frame via runJavaScript(), ensuring it is delivered only to that specific frame.
    • Other frames cannot read this token due to the Same-Origin Policy.
    • Retraversal is triggered by the navigationRequested signal (discovered in 0.0.3a0), with a periodic 2-second interval scan running in parallel as a safeguard.
  • Token Integration Across Slots
    • Added a frame_token parameter to all 16 @Slot methods (ranging from listDevices to various transfer-related methods).
    • Routed the token through _get_open_device(), a bottleneck method that almost all other methods pass through.
    • This fully operationalized the feature, ensuring that "a handle opened by a subframe can continuously and exclusively be used by that subframe."
  • Core Design Rule
    • When wiring the tracker, origins are resolved strictly via tokens without exception, including the main frame.
    • We intentionally left no loopholes like "an empty token defaults to the main frame."
    • Allowing such a loophole would let an adversarial subframe impersonate the main frame simply by invoking the raw QWebChannel directly with an empty token.

Pitfalls & Fixes

Verified on an actual QWebEnginePage, discovering and resolving three implementation pitfalls:

  • runJavaScript() Error
    • Passing only the code string as a single argument throws a "not enough arguments" error. A callback function is strictly required.
  • setHtml() URL Behavior
    • setHtml(html, baseUrl=...) fails to reflect the baseUrl in QWebEngineFrame.url(), treating it as a data: URL instead. Using page.load() works correctly.
  • QWebEngineFrame Identity Issue
    • The QWebEngineFrame object returns a brand-new wrapper every time children() is called.
    • This makes frame identification via id() completely unreliable. We removed the dependency on unique IDs and resolved this by issuing fresh tokens on every scan while enforcing an upper limit on the total token count.

Testing

  • test_frame_origin.py (New, 9 test cases)
    • Includes 8 fast tests using mocks.
    • Includes 1 integration test using a real QWebEnginePage and cross-origin iframes.
  • test_bridge.py Updates (2 test cases added)
    • Added tests for token spoofing rejection and handle isolation between origins.
  • Legacy Test Maintenance
    • Updated existing Python/JS tests to support the new signature (updating 16 instances of the fakeBridge mock).

Note: Since we cannot guarantee stable operation under all conditions yet, please wait for the next version release.

v0.0.3a

v0.0.3a Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 31 Jul 10:47
45af9b3

What was implemented in this update:
・Created errors.py: Centralized DOMException prefix strings (e.g., "SecurityError: ...") into a single function. Previously, these were hardcoded in over 10 different places within bridge.py. This change eliminates a hard-to-find bug where a typo (e.g., "SecurtyError:") wouldn't trigger a syntax error, causing the JavaScript side to silently fall back to an incorrect error name. Dedicated tests (test_errors.py) have also been added.
・PyUSB Version: Confirmed in the previous update that the latest version (1.3.1) is being used. No changes were made here.
・Current Status: Please note that this version may still not be fully functional. Kindly wait for the next release.

v0.0.3

v0.0.3 Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 30 Jul 23:48
45af9b3

Investigation of iframe compatibility and consideration of frame-level origin management.
As such, complete operation is not yet guaranteed. Please look forward to the next release.

v0.0.2b

v0.0.2b Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 30 Jul 06:10
9f61973

New Changes / Updates

Fixed a security vulnerability where cross-origin iframes could hijack top-level page USB permissions.

Previously, install() was configured with script.setRunsOnSubFrames(True), executing the polyfill across all iframes. However, WebUSBBridge._current_origin() relies on QWebEnginePage.url() to determine the origin. Since this always returns the top-level frame's URL, PySide6's public API provides no way to distinguish which specific subframe a request came from via QWebChannel.As a result, untrusted cross-origin iframes (such as ads or third-party embeds) could invoke navigator.usb.requestDevice() or getDevices(), and the request would be treated as originating from the top-level page itself. This allowed malicious iframes to spoof the origin and access any USB device previously authorized by the top-level page, completely breaking the origin isolation model.Since there is currently no reliable way to pinpoint the exact origin per frame, we have taken the safer approach and changed the setting to setRunsOnSubFrames(False) (which prevents navigator.usb from being defined inside iframes). While a real Chrome environment supports WebUSB inside cross-origin iframes with proper isolation, we decided to prioritize security over full functionality since we cannot properly validate it.We also found that install() had zero test coverage. We have newly created tests/test_install.py (utilizing real QWebEngineScript and QWebChannel). We confirmed that reverting this fix causes the newly added tests to fail.

Other 3 fixes discovered (after re-verifying Chrome's usb_device.cc):open() (JS) lacked idempotency, causing Python-side handle leaks when called twice.selectAlternateInterface() completely lacked claim requirement checks.
When a configuration was unselected, claimInterface/releaseInterface incorrectly displayed "Protected Class" as the reason.
Regression tests have been added for all of the above, and we verified they fail if the fixes are reverted.

Current Status & Disclaimer

We are actively looking into a way to properly identify subframe origins.
For now, we are keeping setRunsOnSubFrames(False) for security reasons, but we are working hard to restore setRunsOnSubFrames(True) as soon as a safe method is found.Due to these underlying issues, we cannot guarantee full functionality at this moment.
If you require stable iframe support, please wait for the upcoming version.

v0.0.2a

v0.0.2a Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 29 Jul 22:38
754284e

Changes in this version
Lack of idempotency in open() (JS)In actual Chrome, if a device is already open, it resolves immediately with success.
Our previous implementation called openDevice() every time.
As a result, calling open() twice caused the Python side to keep issuing new handles.
This left the first handle (which included claimed interface information) unclosed, causing a leak.Complete lack of EnsureInterfaceClaimed() equivalent validation in selectAlternateInterface()It was possible to change the alternate setting of any interface without ever calling claimInterface() (meaning it bypassed the protected class checks).
This has been fixed to reject with an InvalidStateError, matching actual Chrome behavior.Lack of explicit EnsureDeviceConfigured() equivalent checks in claimInterface and releaseInterfaceWhen no configuration was selected, the requests were eventually rejected through the interface_class_for failsafe. However, it threw an incorrect error message blaming a "protected class".
This has been corrected to explicitly return InvalidStateError: "the device must have a configuration selected", just like actual Chrome. (Verified: confirmed that reverting this fix actually produces the incorrect SecurityError message).That being said, we still cannot guarantee it will work flawlessly.
Please wait for the next version.

v0.0.2

v0.0.2 Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 29 Jul 09:35
557050e

Changes in this Update
After reviewing the actual Chromium Blink implementation (third_party/blink/renderer/modules/webusb/usb_device.cc), I confirmed that transferIn, transferOut, and clearHalt completely lacked checks equivalent to EnsureEndpointAvailable() found in the production version of Chrome.
To address this, I extended the verification check ("whether the target interface is claimed"), which was previously added only to controlTransferIn/Out, to bulk/interrupt transfers and clearHalt.
This effectively plugs the loophole that allowed direct bulk/interrupt reading and writing to protected class endpoints without going through controlTransfer.
I have also added two regression tests and confirmed that they fail as expected when the fixes are reverted.
Isochronous Transfers (Implemented but Untested on Physical Devices)Although not available in the public pyusb API, I implemented this feature using a workaround that accesses the iso_read/iso_write methods (private internal APIs) of the libusb1 backend via dev._ctx.handle.It only supports uniform packet lengths due to a limitation within the pyusb backend itself.It safely falls back to a NotSupportedError in environments where the backend cannot be used.Since the AI sandbox lacks physical USB hardware, the isochronous transfers themselves could not be verified on an actual device.
However, I have successfully tested the validation logic, packet-splitting arithmetic, and the success paths using a fake backend.
For this reason, it is explicitly marked as "untested" in the README and CHANGELOG.
Therefore, we cannot guarantee that it will work perfectly; please wait for the next version.

v0.0.1b

v0.0.1b Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 28 Jul 22:44
7695877

Fixed minor bugs and refactored some parts, but full stability is not guaranteed. Please wait for the next release.

v0.0.1a

v0.0.1a Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 28 Jul 11:51
e67ad87

Bug Fixes & UpdatesbulkTransferIn:
Fixed a missing IN direction bit (| 0x80), which caused real-device IN transfers to always fail.
requestDeviceChooser: Added a missing re-entrancy guard to prevent duplicate dialog instances in nested event loops.
controlTransferIn/Out: Fixed a critical bug where the protected class validation in claimInterface could be completely bypassed.
(Most Critical)interface_class_for: Fixed a fail-open vulnerability that erroneously identified non-existent interface numbers as "safe."
Added Hub (0x09) to the list of protected interface classes.Implemented USBConfiguration.configurationName and USBAlternateInterface.interfaceName.
Fixed an issue where Control-type endpoints were not being properly excluded.
⚠️ Note: While these fixes have been applied, stable operation is not yet fully guaranteed. Please wait for the next release.

v0.0.1

v0.0.1 Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 27 Jul 05:40
e67ad87

This version briefly explains what was changed.

The requestDeviceChooser method did not exist on the meta-object at all, meaning it did not appear as a method on the JavaScript-side proxy generated by QWebChannel. Meanwhile, _enumerate_filtered_devices was registered with a mismatch between its actual arguments (the usb_core/usb_util pyusb modules and the filters/exclusion_filters lists) and its @Slot(str, result=str) declaration. Because test_bridge.py consistently calls bridge.requestDeviceChooser(...) directly from Python, it always passes regardless of whether the @Slot decorator is present or where it is applied. In fact, even with the bug present, all 24 existing tests passed. This is the exact same blind spot as the previously fixed bug where the @Slot decorator for requestDeviceChooser was entirely missing.

To address this, I added test_requestDeviceChooser_is_registered_as_qt_slot to verify the method by inspecting staticMetaObject and QMetaMethod directly. I have also confirmed that this test successfully fails against a separate copy of the codebase replicating the bug.

However, since this still does not fully guarantee reliable operation, users who prefer a more stable version should wait for the next release.

v0.0.0b

v0.0.0b Pre-release
Pre-release

Choose a tag to compare

@steck0714 steck0714 released this 26 Jul 02:54
a6ed083

We've applied some minor tweaks, but it might still be unstable. Please wait for the next release.