Add detect_media_devices browser module#3565
Merged
zinduolis merged 3 commits intoMay 27, 2026
Merged
Conversation
Implements navigator.mediaDevices.enumerateDevices() detection. Groups results by kind (audioinput, audiooutput, videoinput) and returns counts unconditionally + labels when permissions allow. Handles the no-API case explicitly via beef.status.error(). Closes beefproject#3542
zinduolis
approved these changes
May 27, 2026
Contributor
There was a problem hiding this comment.
Hi @sethumadh , thanks for your PR.
All applicable checks pass:
- RuboCop: clean
- YAML: valid
- RSpec browser suite: 147/147 pass with new module included
- ESLint: not applicable to ERB-templated
command.js(project-wide convention)
The implementation is well-scoped, follows BeEF conventions, has thorough manual testing documented in the PR description, and I have verified it manually on:
- Firefox 148.0.2 (Ubuntu) - pre-permission:
audioinput_count=1&audioinput_labels=&audiooutput_count=0&audiooutput_labels=&videoinput_count=0&videoinput_labels=(confirms audiooutput absent pre-permission) - Firefox 148.0.2 (Ubuntu) - post-permission (
{audio:true}granted): real labels for bothaudioinput(2 entries: device + PulseAudio monitor) andaudiooutput(1 entry), confirming the PR's documented audio-surface bundling behavior - Chrome 148.0.7778.167 (Ubuntu) - pre-permission:
audioinput_count=1&audioinput_labels=&audiooutput_count=1&audiooutput_labels=&videoinput_count=0&videoinput_labels=(confirms all three kinds enumerated)
The module is automatically covered by the existing dynamic test loader.
The PR is now approved and you can merge it. Let me know if you don't have permissions to merge it and I'll do it for you.
Thanks
Contributor
Author
|
@zinduolis Thanks |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Category
Module
Feature/Issue Description
Q: Please give a brief summary of your feature/fix
A: This PR intends to add a new browser-side recon module called
Detect Media Deviceswrappingnavigator.mediaDevices.enumerateDevices()to enumerate the hooked browser's microphones, cameras, and speakers.Pre Permission : Fully passive where no permission prompt is granted by user, no device is opened, no camera/mic indicator activates. The module returns counts for whichever kinds the browser is willing to enumerate pre-permission (please see browser divergence below), with empty-string labels for those kinds.Find the matrix below
Pre-permission behavior matrix
State before any
getUserMediaconsent has been granted to the origin. Assumes the machine has hardware for each kind (the common case).Legend: ⬜ Empty-label placeholder (count capped at 1, "at least one exists") · ❌ Kind absent from enumerated list
audioinput(mic)videoinput(camera)audiooutput(speakers)Key divergence: Chrome enumerates all three kinds pre-permission; Firefox and Safari omit
audiooutputentirely. Firefox shipped this restriction in Firefox 115 (June 2023, Bug #1528042). Safari shipped equivalent restrictions in Safari 14 (per Mozilla's intent-to-ship thread). Chrome has not implemented these restrictions as of writing. All three behaviors are W3C-spec-compliant and the spec is permissive about pre-permission output.When hardware is absent for a kind (e.g., headless Linux, audio-stripped corporate workstation, certain VMs):
audioinputif no microphone hardware exists. Soaudioinput_count=0is a uniform cross-browser signal meaning "no mic on this machine."videoinput:videoinput_count=0means "no camera," uniform across browsers.audiooutput_count=0:** In Chrome it means "no speakers exist on the machine," on Firefox / Safari it means "speakers may or may not exist, can't always mean pre-permission."Post-permission : Post-permission, the module shows real labels and accurate counts for kinds whose consent surface was granted by the user's getUserMedia. Kinds NOT granted permission stay in pre-permission placeholder state (count capped at 1, label empty).
Also for post permission each kind unlocks only when the user gives consent for that specific kind. But audio is an exception. if the permsission is granted for only one audio kind in hooked browser, the permission is bundled for both the audio kind and the enumerate detail will consist of both audio kind, label and count. This is verified across all three major browser engines on macOS. Find the matrix below
Post-permission behavior matrix
Legend: ✅ Real labels exposed, accurate count · ⬜ Empty-label placeholder (count capped at 1) · ❌ Kind absent from enumerated list
getUserMedia(...)withaudioinput(mic)videoinput(camera)audiooutput(speakers){ audio: true }(mic only){ video: true }on Chrome{ video: true }on Firefox / Safari{ audio: true, video: true }Key pattern: The
audiooutputcolumn reveals real labels (✅) only when audio consent was granted and never from video-only consent. The audio surface (audioinput+audiooutput) bundles as one privacy target. The video surface (videoinput) is strictly per-kind. This asymmetry is consistent across all three engines.Closes #3542 (raised by @bcoles).
Q: Give a technical rundown of what you have changed (if applicable)
A: Three new files in the standard 3-file module pattern (
command.js/module.rb/config.yaml), borrowed template frommodules/browser/detect_lastpass/.command.jscallsenumerateDevices(), groups results bykind, and POSTs viabeef.net.send. Errors from Missing-API and exception are reported usingbeef.status.error().module.rbstores six form-body keys into@datastoreandsaves them. No new dependencies.Two design notes:
Multi-key form body, not a single JSON value. BeEF's display layer (
core/main/handlers/commands.rb:83) wraps every result body under a'data':key. A URL-encoded JSON blob renders as%7B%22audioinput%22...— unreadable. Usingaudioinput_count=...&audioinput_labels=...(one key per data point) which is an existing multi-key pattern inwebcam_html5'.Permission-state insight in the count. Pre-permission,
enumerateDevices()returns one placeholder per kind that exists. Post-permission will return the full inventory. So the count alone can be considered as a permission-state signal, independent of labels which are generic pre permission.Please note here re: browser divergence: There's also a vendor signal, meaning this will tell us the kind or type of browsers, in the pre-permission shape: Chrome lists all 3 kinds, while Firefox 115+ and Safari 14+ list only 2 (no
audiooutput), per Mozilla Bug #1528042.Test Cases
Q: Describe your test cases, what you have covered and if there are any use cases that still need addressing.
A: Tested manually on macOS against BeEF's
/demos/basic.htmlhooked demo page. Screenshots below.Chrome


Firefox


Safari


(Built-in)/(Virtual)/(Aggregate)suffixesaudiooutput)Defaultaudiooutput dedupedDefault - <device>audiooutput prefix; surfaces Continuity Camera Desk ViewManual reproduction (5 steps):
http://<beef>:3000/demos/basic.htmlto hook a browser.Detect Media Devices- Execute. Observe the pre-permission result in Module Results History.navigator.mediaDevices.getUserMedia({audio:true, video:true}).then(s => s.getTracks().forEach(t => t.stop()))and click Allow to allow permission to devices.