Skip to content

Commit

Permalink
Prevent Object Navigation Outside of the Lock Screen (#13328)
Browse files Browse the repository at this point in the history
Link to issue number:
None, follow up on #5269

Summary of the issue:
On earlier Windows 10 builds, the top-level Window (Role.WINDOW) of the lock screen cannot directly navigate to the system with object navigation, but its parent can. This was fixed in a commit addressing #5269.

On Windows 11 and newer Windows 10 builds, the top-level Window can directly navigate to the system with object navigation.

STR:

1. Press Windows+L
1. press containing object (NVDA+numpad8/NVDA+shift+upArrow),
1. then you can use next object (NVDA+numpad6/NVDA+shift+rightArrow) to navigate the system.
1. On Windows 10 and 11, using "Navigate to the object under the mouse" (NVDA+numpadMultiply/NVDA+shift+n), you can navigate outside to the system from the lock screen.

Microsoft is aware of this issue.

Description of how this pull request fixes the issue:
This PR adds a function which checks if the lockapp is the foreground window, and if so, if a given object is outside of the lockapp.
To prevent focus objects being set or used for navigation, this function is utilised in various api methods.

An overlay class is also added which prevents navigation and announcement of content outside of the lockapp.

This PR also adds `GlobalCommands.script_navigatorObject_devInfo` to the allowed commands on the lockscreen to aid with debugging.

This command should be safe as:
- The command only logs objects it can navigate to
- The log viewer cannot be accessed from the lockscreen

Testing strategy:
Manual testing on Windows 11, Windows 10 21H2, Windows 10 1809
- Attempt to navigate outside the top level window of the lock screen using object navigation using STR
- Ensure the lock screen can still be navigated with object navigation

An advisory is required to be sent out for earlier NVDA versions.
  • Loading branch information
seanbudd committed Feb 21, 2022
1 parent 23e3c9c commit ab24b5e
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 68 deletions.
143 changes: 101 additions & 42 deletions source/api.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand Down
39 changes: 23 additions & 16 deletions source/appModuleHandler.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
# -*- 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
import ctypes
import ctypes.wintypes
import os
import sys
from typing import (
Dict,
Optional,
)

import winVersion
import pkgutil
import importlib
import threading
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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))

Expand Down
Loading

0 comments on commit ab24b5e

Please sign in to comment.