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 authored Feb 11, 2022
1 parent d482845 commit c29fa56
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 100 deletions.
27 changes: 19 additions & 8 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Babbage B.V.,
# Copyright (C) 2006-2022 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Babbage B.V.,
# Davy Kager
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
Expand All @@ -10,7 +10,6 @@

import os
import time
import re
import typing
import weakref
import core
Expand Down Expand Up @@ -565,6 +564,7 @@ def _get_locationText(self):

#: Typing information for auto-property: _get_parent
parent: typing.Optional['NVDAObject']
"This object's parent (the object that contains this object)."

def _get_parent(self) -> typing.Optional['NVDAObject']:
"""Retrieves this object's parent (the object that contains this object).
Expand All @@ -581,17 +581,23 @@ def _get_container(self):
self.parent = parent
return parent

def _get_next(self):
#: Typing information for auto-property: _get_next
next: typing.Optional['NVDAObject']
"The object directly after this object with the same parent."

def _get_next(self) -> typing.Optional['NVDAObject']:
"""Retrieves the object directly after this object with the same parent.
@return: the next object if it exists else None.
@rtype: L{NVDAObject} or None
"""
return None

def _get_previous(self):
#: Typing information for auto-property: _get_previous
previous: typing.Optional['NVDAObject']
"The object directly before this object with the same parent."

def _get_previous(self) -> typing.Optional['NVDAObject']:
"""Retrieves the object directly before this object with the same parent.
@return: the previous object if it exists else None.
@rtype: L{NVDAObject} or None
"""
return None

Expand Down Expand Up @@ -1296,11 +1302,16 @@ def _formatLongDevInfoString(string, truncateLen=250):
return "%r (truncated)" % string[:truncateLen]
return repr(string)

def _get_devInfo(self):
devInfo: typing.List[str]
"""Information about this object useful to developers."""

# C901 '_get_devInfo' is too complex
# Note: when working on _get_devInfo, look for opportunities to simplify
# and move logic out into smaller helper functions.
def _get_devInfo(self) -> typing.List[str]: # noqa: C901
"""Information about this object useful to developers.
Subclasses may extend this, calling the superclass property first.
@return: A list of text strings providing information about this object useful to developers.
@rtype: list of str
"""
info = []
try:
Expand Down
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-2021 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 Down Expand Up @@ -32,6 +32,38 @@
import documentBase


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 @@ -41,41 +73,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 @@ -176,16 +218,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


Expand Down Expand Up @@ -231,36 +281,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
30 changes: 15 additions & 15 deletions source/appModuleHandler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Joseph Lee,
# 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
"""

from __future__ import annotations
Expand All @@ -17,6 +16,7 @@
import os
import sys
from typing import (
Dict,
Optional,
)

Expand All @@ -27,10 +27,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
Expand All @@ -40,8 +38,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 @@ -108,12 +106,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 @@ -339,18 +336,19 @@ 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: Optional[ctypes.c_long] = None
"""RPC binding handle pointing to the RPC server for this process"""

Expand Down Expand Up @@ -431,6 +429,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 c29fa56

Please sign in to comment.