diff --git a/source/api.py b/source/api.py index b25cc595848..5a45551a5fb 100644 --- a/source/api.py +++ b/source/api.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, James Teh, Michael Curran, Peter Vagner, Derek Riemer, +# Copyright (C) 2006-2022 NV Access Limited, James Teh, Michael Curran, Peter Vagner, Derek Riemer, # Davy Kager, Babbage B.V., Leonard de Ruijter, Joseph Lee, Accessolutions, Julien Cochuyt # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -25,6 +25,38 @@ from typing import Any, Optional +def _isLockAppAndAlive(appModule: "appModuleHandler.AppModule"): + return appModule.appName == "lockapp" and appModule.isAlive + + +def _isSecureObjectWhileLockScreenActivated(obj: NVDAObjects.NVDAObject) -> bool: + """ + While Windows is locked, Windows 10 and 11 allow for object navigation outside of the lockscreen. + @return: C{True} if the Windows 10/11 lockscreen is active and C{obj} is outside of the lockscreen. + + According to MS docs, "There is no function you can call to determine whether the workstation is locked." + https://docs.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-lockworkstation + """ + runningAppModules = appModuleHandler.runningTable.values() + lockAppModule = next(filter(_isLockAppAndAlive, runningAppModules), None) + if lockAppModule is None: + return False + + # The LockApp process might be kept alive + # So determine if it is active, check the foreground window + foregroundHWND = winUser.getForegroundWindow() + foregroundProcessId, _threadId = winUser.getWindowThreadProcessID(foregroundHWND) + + isLockAppForeground = foregroundProcessId == lockAppModule.processID + isObjectOutsideLockApp = obj.appModule.processID != foregroundProcessId + + if isLockAppForeground and isObjectOutsideLockApp: + if log.isEnabledFor(log.DEBUG): + devInfo = '\n'.join(obj.devInfo) + log.debug(f"Attempt at navigating to a secure object: {devInfo}") + return True + return False + #User functions def getFocusObject() -> NVDAObjects.NVDAObject: @@ -34,41 +66,51 @@ def getFocusObject() -> NVDAObjects.NVDAObject: """ return globalVars.focusObject -def getForegroundObject(): + +def getForegroundObject() -> NVDAObjects.NVDAObject: """Gets the current foreground object. This (cached) object is the (effective) top-level "window" (hwnd). EG a Dialog rather than the focused control within the dialog. The cache is updated as queued events are processed, as such there will be a delay between the winEvent and this function matching. However, within NVDA this should be used in order to be in sync with other functions such as "getFocusAncestors". -@returns: the current foreground object -@rtype: L{NVDAObjects.NVDAObject} -""" + @returns: the current foreground object + """ return globalVars.foregroundObject -def setForegroundObject(obj): + +def setForegroundObject(obj: NVDAObjects.NVDAObject) -> bool: """Stores the given object as the current foreground object. Note: does not cause the operating system to change the foreground window, but simply allows NVDA to keep track of what the foreground window is. Alternative names for this function may have been: - setLastForegroundWindow - setLastForegroundEventObject -@param obj: the object that will be stored as the current foreground object -@type obj: NVDAObjects.NVDAObject -""" + @param obj: the object that will be stored as the current foreground object + """ if not isinstance(obj,NVDAObjects.NVDAObject): return False + if _isSecureObjectWhileLockScreenActivated(obj): + return False globalVars.foregroundObject=obj return True -def setFocusObject(obj): - """Stores an object as the current focus object. (Note: this does not physically change the window with focus in the operating system, but allows NVDA to keep track of the correct object). -Before overriding the last object, this function calls event_loseFocus on the object to notify it that it is loosing focus. -@param obj: the object that will be stored as the focus object -@type obj: NVDAObjects.NVDAObject -""" + +# C901 'setFocusObject' is too complex +# Note: when working on setFocusObject, look for opportunities to simplify +# and move logic out into smaller helper functions. +def setFocusObject(obj: NVDAObjects.NVDAObject) -> bool: # noqa: C901 + """Stores an object as the current focus object. + Note: this does not physically change the window with focus in the operating system, + but allows NVDA to keep track of the correct object. + Before overriding the last object, + this function calls event_loseFocus on the object to notify it that it is losing focus. + @param obj: the object that will be stored as the focus object + """ if not isinstance(obj,NVDAObjects.NVDAObject): return False + if _isSecureObjectWhileLockScreenActivated(obj): + return False if globalVars.focusObject: eventHandler.executeEvent("loseFocus",globalVars.focusObject) oldFocusLine=globalVars.focusAncestors @@ -169,16 +211,24 @@ def getMouseObject(): """Returns the object that is directly under the mouse""" return globalVars.mouseObject -def setMouseObject(obj): + +def setMouseObject(obj: NVDAObjects.NVDAObject) -> None: """Tells NVDA to remember the given object as the object that is directly under the mouse""" + if _isSecureObjectWhileLockScreenActivated(obj): + return globalVars.mouseObject=obj -def getDesktopObject(): + +def getDesktopObject() -> NVDAObjects.NVDAObject: """Get the desktop object""" return globalVars.desktopObject -def setDesktopObject(obj): - """Tells NVDA to remember the given object as the desktop object""" + +def setDesktopObject(obj: NVDAObjects.NVDAObject) -> None: + """Tells NVDA to remember the given object as the desktop object. + We cannot prevent setting this when _isSecureObjectWhileLockScreenActivated is True, + as NVDA needs to set the desktopObject on start, and NVDA may start from the lockscreen. + """ globalVars.desktopObject=obj def getReviewPosition(): @@ -222,36 +272,45 @@ def setReviewPosition( visionContext = vision.constants.Context.REVIEW vision.handler.handleReviewMove(context=visionContext) -def getNavigatorObject(): - """Gets the current navigator object. Navigator objects can be used to navigate around the operating system (with the number pad) with out moving the focus. If the navigator object is not set, it fetches it from the review position. -@returns: the current navigator object -@rtype: L{NVDAObjects.NVDAObject} -""" + +def getNavigatorObject() -> NVDAObjects.NVDAObject: + """Gets the current navigator object. + Navigator objects can be used to navigate around the operating system (with the numpad), + without moving the focus. + If the navigator object is not set, it fetches and sets it from the review position. + @returns: the current navigator object + """ if globalVars.navigatorObject: return globalVars.navigatorObject + elif review.getCurrentMode() == 'object': + obj = globalVars.reviewPosition.obj else: - if review.getCurrentMode()=='object': - obj=globalVars.reviewPosition.obj - else: - try: - obj=globalVars.reviewPosition.NVDAObjectAtStart - except (NotImplementedError,LookupError): - obj=globalVars.reviewPosition.obj - globalVars.navigatorObject=getattr(obj,'rootNVDAObject',None) or obj + try: + obj = globalVars.reviewPosition.NVDAObjectAtStart + except (NotImplementedError, LookupError): + obj = globalVars.reviewPosition.obj + nextObj = getattr(obj, 'rootNVDAObject', None) or obj + if _isSecureObjectWhileLockScreenActivated(nextObj): return globalVars.navigatorObject + globalVars.navigatorObject = nextObj + return globalVars.navigatorObject + + +def setNavigatorObject(obj: NVDAObjects.NVDAObject, isFocus: bool = False) -> Optional[bool]: + """Sets an object to be the current navigator object. + Navigator objects can be used to navigate around the operating system (with the numpad), + without moving the focus. + It also sets the current review position to None so that next time the review position is asked for, + it is created from the navigator object. + @param obj: the object that will be set as the current navigator object + @param isFocus: true if the navigator object was set due to a focus change. + """ -def setNavigatorObject(obj,isFocus=False): - """Sets an object to be the current navigator object. Navigator objects can be used to navigate around the operating system (with the number pad) with out moving the focus. It also sets the current review position to None so that next time the review position is asked for, it is created from the navigator object. -@param obj: the object that will be set as the current navigator object -@type obj: NVDAObjects.NVDAObject -@param isFocus: true if the navigator object was set due to a focus change. -@type isFocus: bool -""" - if not isinstance(obj,NVDAObjects.NVDAObject): + if not isinstance(obj, NVDAObjects.NVDAObject): + return False + if _isSecureObjectWhileLockScreenActivated(obj): return False globalVars.navigatorObject=obj - oldPos=globalVars.reviewPosition - oldPosObj=globalVars.reviewPositionObj globalVars.reviewPosition=None globalVars.reviewPositionObj=None reviewMode=review.getCurrentMode() diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py index 463d87bb190..aa047b61ce5 100644 --- a/source/appModuleHandler.py +++ b/source/appModuleHandler.py @@ -1,13 +1,12 @@ # -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Joseph Lee, -# Babbage B.V., Mozilla Corporation +# Copyright (C) 2006-2022 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Joseph Lee, +# Babbage B.V., Mozilla Corporation, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. """Manages appModules. @var runningTable: a dictionary of the currently running appModules, using their application's main window handle as a key. -@type runningTable: dict """ import itertools @@ -15,6 +14,11 @@ import ctypes.wintypes import os import sys +from typing import ( + Dict, + Optional, +) + import winVersion import pkgutil import importlib @@ -22,10 +26,8 @@ import tempfile import comtypes.client import baseObject -import globalVars from logHandler import log import NVDAHelper -import winUser import winKernel import config import NVDAObjects #Catches errors before loading default appModule @@ -35,8 +37,8 @@ import extensionPoints from fileUtils import getFileVersionInfo -#Dictionary of processID:appModule paires used to hold the currently running modules -runningTable={} +# Dictionary of processID:appModule pairs used to hold the currently running modules +runningTable: Dict[int, AppModule] = {} #: The process ID of NVDA itself. NVDAProcessID=None _importers=None @@ -103,12 +105,11 @@ def getAppModuleForNVDAObject(obj): return return getAppModuleFromProcessID(obj.processID) -def getAppModuleFromProcessID(processID): + +def getAppModuleFromProcessID(processID: int) -> AppModule: """Finds the appModule that is for the given process ID. The module is also cached for later retreavals. @param processID: The ID of the process for which you wish to find the appModule. - @type processID: int - @returns: the appModule, or None if there isn't one - @rtype: appModule + @returns: the appModule """ with _getAppModuleLock: mod=runningTable.get(processID) @@ -334,18 +335,22 @@ class AppModule(baseObject.ScriptableObject): #: @type: bool sleepMode=False + processID: int + """The ID of the process this appModule is for""" + + appName: str + """The application name""" + def __init__(self,processID,appName=None): super(AppModule,self).__init__() - #: The ID of the process this appModule is for. - #: @type: int self.processID=processID if appName is None: appName=getAppNameFromProcessID(processID) - #: The application name. - #: @type: str self.appName=appName self.processHandle=winKernel.openProcess(winKernel.SYNCHRONIZE|winKernel.PROCESS_QUERY_INFORMATION,False,processID) - self.helperLocalBindingHandle=None + self.helperLocalBindingHandle: Optional[ctypes.c_long] = None + """RPC binding handle pointing to the RPC server for this process""" + self._inprocRegistrationHandle=None def _getExecutableFileInfo(self): @@ -423,6 +428,8 @@ def __repr__(self): def _get_appModuleName(self): return self.__class__.__module__.split('.')[-1] + isAlive: bool + def _get_isAlive(self): return bool(winKernel.waitForSingleObject(self.processHandle,0)) diff --git a/source/appModules/lockapp.py b/source/appModules/lockapp.py index 831fa66ff75..72c55c2a2b3 100644 --- a/source/appModules/lockapp.py +++ b/source/appModules/lockapp.py @@ -1,37 +1,72 @@ -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2015 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2015-2022 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +from typing import Callable, Optional import appModuleHandler import controlTypes import inputCore import api import eventHandler import config +import NVDAObjects from NVDAObjects.UIA import UIA from globalCommands import GlobalCommands -"""App module for the Windows 10 lock screen. +"""App module for the Windows 10 and 11 lock screen. The lock screen runs as the logged in user on the default desktop, so we need to explicitly stop people from accessing/changing things outside of the lock screen. +This is done in the api module by utilizing _isSecureObjectWhileLockScreenActivated. """ -# Windows 10 lock screen container + +# Windows 10 and 11 lock screen container class LockAppContainer(UIA): # Make sure the user can get to this so they can dismiss the lock screen from a touch screen. presentationType=UIA.presType_content + +# Any object in the Lock App +class LockAppObject(NVDAObjects.NVDAObject): + """ + Prevent users from object navigating outside of the lock screen. + While usages of `api._isSecureObjectWhileLockScreenActivated` in the api module prevent + the user from moving to the object, this overlay class prevents reading the objects. + """ + + def _get_next(self) -> Optional[NVDAObjects.NVDAObject]: + nextObject = super()._get_next() + if nextObject and nextObject.appModule.appName == self.appModule.appName: + return nextObject + return None + + def _get_previous(self) -> Optional[NVDAObjects.NVDAObject]: + previousObject = super()._get_previous() + if previousObject and previousObject.appModule.appName == self.appModule.appName: + return previousObject + return None + + def _get_parent(self) -> Optional[NVDAObjects.NVDAObject]: + parentObject = super()._get_parent() + if parentObject and parentObject.appModule.appName == self.appModule.appName: + return parentObject + return None + + class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self,obj,clsList): if isinstance(obj,UIA) and obj.role==controlTypes.Role.PANE and obj.UIAElement.cachedClassName=="LockAppContainer": clsList.insert(0,LockAppContainer) + clsList.insert(0, LockAppObject) - def event_NVDAObject_init(self, obj): - if obj.role == controlTypes.Role.WINDOW: - # Stop users from being able to object navigate out of the lock screen. - obj.parent = None + def event_foreground(self, obj: NVDAObjects.NVDAObject, nextHandler: Callable[[], None]): + """Set mouse object explicitly before continuing to the next handler. + This is to prevent the mouse focus remaining on the desktop when locking the screen. + """ + api.setMouseObject(obj) + nextHandler() SAFE_SCRIPTS = { GlobalCommands.script_reportCurrentFocus, @@ -46,6 +81,7 @@ def event_NVDAObject_init(self, obj): GlobalCommands.script_navigatorObject_next, GlobalCommands.script_navigatorObject_previous, GlobalCommands.script_navigatorObject_firstChild, + GlobalCommands.script_navigatorObject_devInfo, GlobalCommands.script_review_activate, GlobalCommands.script_review_top, GlobalCommands.script_review_previousLine, diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 096c74a5dcb..a0f19fe7975 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,6 +3,17 @@ What's New in NVDA %!includeconf: ../changes.t2tconf + += 2021.3.2 = +This is a minor release to fix several security issues raised since the 2021.3 release cycle. +Please responsibly disclose security issues to info@nvaccess.org. + + +== Bug Fixes == +- Security fix: Prevent object navigation outside of the lockscreen on Windows 10 and Windows 11. (#13328) +- + + = 2021.3.1 = This is a minor release to fix several issues in 2021.3.