motioneye icon indicating copy to clipboard operation
motioneye copied to clipboard

Fix uncontrolled command line in V4L2

Open odaysec opened this issue 6 months ago • 1 comments

https://github.com/motioneye-project/motioneye/blob/5158ddc8fb3d9925f11d078ae55654864a28edd0/motioneye/controls/v4l2ctl.py#L98-L98

https://github.com/motioneye-project/motioneye/blob/5158ddc8fb3d9925f11d078ae55654864a28edd0/motioneye/utils/init.py#L664-L664

Fix the issue need to ensure that user-provided input is sanitized or validated before being passed to subprocess.run. The best approach is to implement an allowlist of acceptable commands or arguments and reject any input that does not match the allowlist. Additionally, we should avoid using shell=True wherever possible, as it significantly increases the risk of command injection.

Steps to fix:

  1. Introduce an allowlist of valid commands or arguments in motioneye/controls/v4l2ctl.py for the list_resolutions function.
  2. Validate the device parameter against this allowlist before constructing the cmd string.
  3. Modify call_subprocess in motioneye/utils/__init__.py to reject unsafe inputs or enforce stricter validation.
  4. Ensure that all paths leading to call_subprocess sanitize or validate user input.

Code that passes user input directly to exec, eval, or some other library routine that executes a command, allows the user to execute malicious code. The following shows two functions. The first is unsafe as it takes a shell script that can be changed by a user, and passes it straight to subprocess.call() without examining it first. The second is safe as it selects the command from a predefined allowlist.

urlpatterns = [
    # Route to command_execution
    url(r'^command-ex1$', command_execution_unsafe, name='command-execution-unsafe'),
    url(r'^command-ex2$', command_execution_safe, name='command-execution-safe')
]

COMMANDS = {
    "list" :"ls",
    "stat" : "stat"
}

def command_execution_unsafe(request):
    if request.method == 'POST':
        action = request.POST.get('action', '')
        #BAD -- No sanitizing of input
        subprocess.call(["application", action])

def command_execution_safe(request):
    if request.method == 'POST':
        action = request.POST.get('action', '')
        #GOOD -- Use an allowlist
        subprocess.call(["application", COMMANDS[action]])

odaysec avatar Jun 09 '25 06:06 odaysec

IMO there is no point to sanitize call_subprocess. It really is just a wrapper for subprocess.run, changing some defaults. Any harmful code could just call subprocess.run directly, bypassing the sanity check.

But it generally makes sense to sanitize any kind of user input, including output of external commands. I am not 100% sure, but AFAIK v4l2-ctl --list-devices is supposed to list /dev/video[0-9]+ device paths only, so we could do a sanity check with regex, instead of a hardcoded allow list. Regarding security, at least, thanks to shlex.quote(), no matter what the device name is, any unintended expansion or code execution is ruled out, and the whole input is assured to be taken literally as device name argument for the command. Worst that can happen is that v4l2-ctl complains about the input not being a valid video device. Quite possible that this error is even better than what we could achieve with a sanity check.

I'll turn this into a draft, since with hardcoded example allow lists, it is not ready to be tested, but a discussion basis.


Regarding shell=True: yes, we should ban it entirely. Where used, we should convert any logic done in the shell into logic done in Python. We are happily accepting any PR, whether it replaces all or just a single shell=True.

MichaIng avatar Jun 09 '25 12:06 MichaIng