macos-accessibility by martinholovsky/claude-skills-generator
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill macos-accessibility风险等级 : 高 - 系统级访问、TCC 权限要求、进程交互
您是 macOS 辅助功能自动化领域的专家,在以下方面拥有深厚的专业知识:
执行辅助功能自动化时:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
所有自动化必须:
每个自动化操作必须:
主要框架 : ApplicationServices / HIServices
关键依赖项 :
ApplicationServices.framework # 核心辅助功能 API
CoreFoundation.framework # CFType 支持
AppKit.framework # NSRunningApplication
Security.framework # TCC 查询
| 库 | 用途 | 安全注意事项 |
|---|---|---|
pyobjc-framework-ApplicationServices | Python 绑定 | 验证元素访问 |
atomac | 高级包装器 | 使用前检查 TCC |
pyautogui | 输入模拟 | 需要辅助功能权限 |
import subprocess
from ApplicationServices import (
AXIsProcessTrustedWithOptions,
kAXTrustedCheckOptionPrompt
)
class TCCValidator:
"""在自动化之前验证 TCC 权限。"""
@staticmethod
def check_accessibility_permission(prompt: bool = False) -> bool:
"""检查进程是否具有辅助功能权限。"""
options = {kAXTrustedCheckOptionPrompt: prompt}
return AXIsProcessTrustedWithOptions(options)
@staticmethod
def get_tcc_status(bundle_id: str) -> str:
"""查询 TCC 数据库以获取权限状态。"""
query = f"""
SELECT client, auth_value FROM access
WHERE service = 'kTCCServiceAccessibility'
AND client = '{bundle_id}'
"""
# 注意:直接访问 TCC 数据库需要禁用 SIP
# 正常操作请使用 AXIsProcessTrusted
pass
def ensure_permission(self):
"""确保授予辅助功能权限。"""
if not self.check_accessibility_permission():
raise PermissionError(
"需要辅助功能权限。"
"请在系统偏好设置 > 安全性与隐私 > 辅助功能中启用"
)
from ApplicationServices import (
AXUIElementCreateSystemWide,
AXUIElementCreateApplication,
AXUIElementCopyAttributeValue,
AXUIElementCopyAttributeNames,
)
from Quartz import kAXErrorSuccess
import logging
class SecureAXAutomation:
"""AXUIElement 自动化的安全包装器。"""
BLOCKED_APPS = {
'com.apple.keychainaccess', # 钥匙串访问
'com.apple.systempreferences', # 系统偏好设置
'com.apple.SecurityAgent', # 安全对话框
'com.apple.Terminal', # 终端
'com.1password.1password', # 1Password
}
def __init__(self, permission_tier: str = 'read-only'):
self.permission_tier = permission_tier
self.logger = logging.getLogger('ax.security')
self.operation_timeout = 30
# 初始化时验证 TCC 权限
if not TCCValidator.check_accessibility_permission():
raise PermissionError("需要辅助功能权限")
def get_application_element(self, pid: int) -> 'AXUIElementRef':
"""获取应用程序元素并进行验证。"""
# 获取 Bundle ID
bundle_id = self._get_bundle_id(pid)
# 安全检查
if bundle_id in self.BLOCKED_APPS:
self.logger.warning(
'blocked_app_access',
bundle_id=bundle_id,
reason='security_policy'
)
raise SecurityError(f"对 {bundle_id} 的访问被阻止")
# 创建元素
app_element = AXUIElementCreateApplication(pid)
self._audit_log('app_element_created', bundle_id, pid)
return app_element
def get_attribute(self, element, attribute: str):
"""获取元素属性并进行安全过滤。"""
sensitive = ['AXValue', 'AXSelectedText', 'AXDocument']
if attribute in sensitive and self.permission_tier == 'read-only':
raise SecurityError(f"访问 {attribute} 需要提升的权限")
error, value = AXUIElementCopyAttributeValue(element, attribute, None)
if error != kAXErrorSuccess:
return None
# 对密码值进行脱敏处理
return '[REDACTED]' if 'password' in str(attribute).lower() else value
def _audit_log(self, action: str, bundle_id: str, pid: int):
self.logger.info(f'ax.{action}', extra={
'bundle_id': bundle_id, 'pid': pid, 'permission_tier': self.permission_tier
})
from ApplicationServices import AXUIElementPerformAction
class SafeActionExecutor:
"""使用安全控制执行 AX 操作。"""
BLOCKED_ACTIONS = {
'read-only': ['AXPress', 'AXIncrement', 'AXDecrement', 'AXConfirm'],
'standard': ['AXDelete', 'AXCancel'],
}
def __init__(self, permission_tier: str):
self.permission_tier = permission_tier
def perform_action(self, element, action: str):
blocked = self.BLOCKED_ACTIONS.get(self.permission_tier, [])
if action in blocked:
raise PermissionError(f"操作 {action} 在 {self.permission_tier} 层级不被允许")
error = AXUIElementPerformAction(element, action)
return error == kAXErrorSuccess
from AppKit import NSWorkspace, NSRunningApplication
class ApplicationMonitor:
"""监控和验证正在运行的应用程序。"""
def get_frontmost_app(self) -> dict:
app = NSWorkspace.sharedWorkspace().frontmostApplication()
return {
'pid': app.processIdentifier(),
'bundle_id': app.bundleIdentifier(),
'name': app.localizedName(),
}
def validate_application(self, pid: int) -> bool:
app = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
if not app or app.bundleIdentifier() in SecureAXAutomation.BLOCKED_APPS:
return False
# 验证代码签名
result = subprocess.run(['codesign', '-v', app.bundleURL().path()], capture_output=True)
return result.returncode == 0
# tests/test_ax_automation.py
import pytest
from unittest.mock import patch, MagicMock
class TestTCCValidation:
def test_raises_error_when_permission_missing(self):
with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=False):
with pytest.raises(PermissionError) as exc:
SecureAXAutomation()
assert "需要辅助功能权限" in str(exc.value)
class TestSecureElementDiscovery:
def test_blocks_keychain_access(self):
with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=True):
automation = SecureAXAutomation()
with pytest.raises(SecurityError):
automation.get_application_element(pid=1234) # Keychain PID
def test_filters_sensitive_attributes(self):
automation = SecureAXAutomation(permission_tier='read-only')
result = automation.get_attribute(MagicMock(), 'AXPasswordField')
assert result == '[REDACTED]'
class TestActionExecution:
def test_blocks_actions_in_readonly_tier(self):
executor = SafeActionExecutor(permission_tier='read-only')
with pytest.raises(PermissionError):
executor.perform_action(MagicMock(), 'AXPress')
实现使测试通过的类和方法。
应用安全模式、缓存和错误处理。
# 运行所有测试并计算覆盖率
pytest tests/ -v --cov=ax_automation --cov-report=term-missing
# 运行特定于安全的测试
pytest tests/test_ax_automation.py -k "security or permission" -v
# 使用超时运行以捕获挂起
pytest tests/ --timeout=30
# 错误做法:重复查询
element = AXUIElementCreateApplication(pid) # 每次调用
# 正确做法:使用 TTL 缓存
class ElementCache:
def __init__(self, ttl=5.0):
self.cache, self.ttl = {}, ttl
def get_or_create(self, pid, role):
key = (pid, role)
if key in self.cache and time() - self.cache[key][1] < self.ttl:
return self.cache[key][0]
element = self._create_element(pid, role)
self.cache[key] = (element, time())
return element
# 错误做法:搜索整个层次结构
find_all_children(app_element, role='AXButton') # 深度搜索
# 正确做法:限制深度
def find_button(element, max_depth=3, depth=0, results=None):
if results is None: results = []
if depth > max_depth: return results
if get_attribute(element, 'AXRole') == 'AXButton':
results.append(element)
else:
for child in get_attribute(element, 'AXChildren') or []:
find_button(child, max_depth, depth+1, results)
return results
# 错误做法:顺序阻塞
for app in apps: windows.extend(get_windows(app))
# 正确做法:使用 ThreadPoolExecutor 并发
async def get_all_windows_async():
with ThreadPoolExecutor(max_workers=4) as executor:
tasks = [loop.run_in_executor(executor, get_windows, app) for app in apps]
results = await asyncio.gather(*tasks)
return [w for wins in results for w in wins]
# 错误做法:多次调用
title = AXUIElementCopyAttributeValue(element, 'AXTitle', None)
role = AXUIElementCopyAttributeValue(element, 'AXRole', None)
# 正确做法:批量查询
error, values = AXUIElementCopyMultipleAttributeValues(
element, ['AXTitle', 'AXRole', 'AXPosition', 'AXSize'], None
)
info = dict(zip(attributes, values)) if error == kAXErrorSuccess else {}
# 错误做法:为每个通知设置观察者而不进行防抖
# 正确做法:带有防抖的选择性观察者
class OptimizedObserver:
def __init__(self, app_element, notifications):
self.last_callback, self.debounce_ms = {}, 100
for notif in notifications:
add_observer(app_element, notif, self._debounced_callback)
def _debounced_callback(self, notification, element):
now = time() * 1000
if now - self.last_callback.get(notification, 0) < self.debounce_ms:
return
self.last_callback[notification] = now
self._handle_notification(notification, element)
| CVE/CWE | 严重性 | 描述 | 缓解措施 |
|---|---|---|---|
| CVE-2023-32364 | 严重 | 通过符号链接绕过 TCC | 更新 macOS,验证路径 |
| CVE-2023-28206 | 高 | AX 权限提升 | 进程验证,代码签名 |
| CWE-290 | 高 | Bundle ID 欺骗 | 验证代码签名 |
| CWE-74 | 高 | 通过 AX 进行输入注入 | 阻止 SecurityAgent |
| CVE-2022-42796 | 中 | 强化运行时绕过 | 验证目标应用程序运行时 |
| OWASP | 风险 | 缓解措施 |
|---|---|---|
| A01 访问控制失效 | 严重 | TCC 验证,阻止列表 |
| A02 安全配置错误 | 高 | 最小权限 |
| A05 注入 | 高 | 输入验证 |
| A07 身份验证和会话管理失效 | 高 | 代码签名验证 |
| 层级 | 属性 | 操作 | 超时 |
|---|---|---|---|
| read-only | AXTitle, AXRole, AXChildren | 无 | 30s |
| standard | 全部 | AXPress, AXIncrement | 60s |
| elevated | 全部 | 全部(SecurityAgent 除外) | 120s |
关键反模式 - 始终避免:
pytest tests/ -vpytest -k "security or permission"pytest --cov --cov-fail-under=80您的目标是创建 macOS 辅助功能自动化,使其:
安全提醒 :
references/advanced-patterns.mdreferences/security-examples.mdreferences/threat-model.md每周安装次数
161
代码仓库
GitHub 星标数
29
首次出现
Jan 20, 2026
安全审计
安装于
gemini-cli136
opencode136
codex136
github-copilot125
cursor123
amp104
Risk Level : HIGH - System-level access, TCC permission requirements, process interaction
You are an expert in macOS Accessibility automation with deep expertise in:
When performing accessibility automation:
All automation must:
Every automation operation MUST:
Primary Framework : ApplicationServices / HIServices
Key Dependencies :
ApplicationServices.framework # Core accessibility APIs
CoreFoundation.framework # CFType support
AppKit.framework # NSRunningApplication
Security.framework # TCC queries
| Library | Purpose | Security Notes |
|---|---|---|
pyobjc-framework-ApplicationServices | Python bindings | Validate element access |
atomac | Higher-level wrapper | Check TCC before use |
pyautogui | Input simulation | Requires Accessibility permission |
import subprocess
from ApplicationServices import (
AXIsProcessTrustedWithOptions,
kAXTrustedCheckOptionPrompt
)
class TCCValidator:
"""Validate TCC permissions before automation."""
@staticmethod
def check_accessibility_permission(prompt: bool = False) -> bool:
"""Check if process has accessibility permission."""
options = {kAXTrustedCheckOptionPrompt: prompt}
return AXIsProcessTrustedWithOptions(options)
@staticmethod
def get_tcc_status(bundle_id: str) -> str:
"""Query TCC database for permission status."""
query = f"""
SELECT client, auth_value FROM access
WHERE service = 'kTCCServiceAccessibility'
AND client = '{bundle_id}'
"""
# Note: Direct TCC database access requires SIP disabled
# Use AXIsProcessTrusted for normal operation
pass
def ensure_permission(self):
"""Ensure accessibility permission is granted."""
if not self.check_accessibility_permission():
raise PermissionError(
"Accessibility permission required. "
"Enable in System Preferences > Security & Privacy > Accessibility"
)
from ApplicationServices import (
AXUIElementCreateSystemWide,
AXUIElementCreateApplication,
AXUIElementCopyAttributeValue,
AXUIElementCopyAttributeNames,
)
from Quartz import kAXErrorSuccess
import logging
class SecureAXAutomation:
"""Secure wrapper for AXUIElement automation."""
BLOCKED_APPS = {
'com.apple.keychainaccess', # Keychain Access
'com.apple.systempreferences', # System Preferences
'com.apple.SecurityAgent', # Security dialogs
'com.apple.Terminal', # Terminal
'com.1password.1password', # 1Password
}
def __init__(self, permission_tier: str = 'read-only'):
self.permission_tier = permission_tier
self.logger = logging.getLogger('ax.security')
self.operation_timeout = 30
# Validate TCC permission on init
if not TCCValidator.check_accessibility_permission():
raise PermissionError("Accessibility permission required")
def get_application_element(self, pid: int) -> 'AXUIElementRef':
"""Get application element with validation."""
# Get bundle ID
bundle_id = self._get_bundle_id(pid)
# Security check
if bundle_id in self.BLOCKED_APPS:
self.logger.warning(
'blocked_app_access',
bundle_id=bundle_id,
reason='security_policy'
)
raise SecurityError(f"Access to {bundle_id} is blocked")
# Create element
app_element = AXUIElementCreateApplication(pid)
self._audit_log('app_element_created', bundle_id, pid)
return app_element
def get_attribute(self, element, attribute: str):
"""Get element attribute with security filtering."""
sensitive = ['AXValue', 'AXSelectedText', 'AXDocument']
if attribute in sensitive and self.permission_tier == 'read-only':
raise SecurityError(f"Access to {attribute} requires elevated permissions")
error, value = AXUIElementCopyAttributeValue(element, attribute, None)
if error != kAXErrorSuccess:
return None
# Redact password values
return '[REDACTED]' if 'password' in str(attribute).lower() else value
def _audit_log(self, action: str, bundle_id: str, pid: int):
self.logger.info(f'ax.{action}', extra={
'bundle_id': bundle_id, 'pid': pid, 'permission_tier': self.permission_tier
})
from ApplicationServices import AXUIElementPerformAction
class SafeActionExecutor:
"""Execute AX actions with security controls."""
BLOCKED_ACTIONS = {
'read-only': ['AXPress', 'AXIncrement', 'AXDecrement', 'AXConfirm'],
'standard': ['AXDelete', 'AXCancel'],
}
def __init__(self, permission_tier: str):
self.permission_tier = permission_tier
def perform_action(self, element, action: str):
blocked = self.BLOCKED_ACTIONS.get(self.permission_tier, [])
if action in blocked:
raise PermissionError(f"Action {action} not allowed in {self.permission_tier} tier")
error = AXUIElementPerformAction(element, action)
return error == kAXErrorSuccess
from AppKit import NSWorkspace, NSRunningApplication
class ApplicationMonitor:
"""Monitor and validate running applications."""
def get_frontmost_app(self) -> dict:
app = NSWorkspace.sharedWorkspace().frontmostApplication()
return {
'pid': app.processIdentifier(),
'bundle_id': app.bundleIdentifier(),
'name': app.localizedName(),
}
def validate_application(self, pid: int) -> bool:
app = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
if not app or app.bundleIdentifier() in SecureAXAutomation.BLOCKED_APPS:
return False
# Verify code signature
result = subprocess.run(['codesign', '-v', app.bundleURL().path()], capture_output=True)
return result.returncode == 0
# tests/test_ax_automation.py
import pytest
from unittest.mock import patch, MagicMock
class TestTCCValidation:
def test_raises_error_when_permission_missing(self):
with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=False):
with pytest.raises(PermissionError) as exc:
SecureAXAutomation()
assert "Accessibility permission required" in str(exc.value)
class TestSecureElementDiscovery:
def test_blocks_keychain_access(self):
with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=True):
automation = SecureAXAutomation()
with pytest.raises(SecurityError):
automation.get_application_element(pid=1234) # Keychain PID
def test_filters_sensitive_attributes(self):
automation = SecureAXAutomation(permission_tier='read-only')
result = automation.get_attribute(MagicMock(), 'AXPasswordField')
assert result == '[REDACTED]'
class TestActionExecution:
def test_blocks_actions_in_readonly_tier(self):
executor = SafeActionExecutor(permission_tier='read-only')
with pytest.raises(PermissionError):
executor.perform_action(MagicMock(), 'AXPress')
Implement the classes and methods that make tests pass.
Apply security patterns, caching, and error handling.
# Run all tests with coverage
pytest tests/ -v --cov=ax_automation --cov-report=term-missing
# Run security-specific tests
pytest tests/test_ax_automation.py -k "security or permission" -v
# Run with timeout to catch hangs
pytest tests/ --timeout=30
# BAD: Query repeatedly
element = AXUIElementCreateApplication(pid) # Each call
# GOOD: Cache with TTL
class ElementCache:
def __init__(self, ttl=5.0):
self.cache, self.ttl = {}, ttl
def get_or_create(self, pid, role):
key = (pid, role)
if key in self.cache and time() - self.cache[key][1] < self.ttl:
return self.cache[key][0]
element = self._create_element(pid, role)
self.cache[key] = (element, time())
return element
# BAD: Search entire hierarchy
find_all_children(app_element, role='AXButton') # Deep search
# GOOD: Limit depth
def find_button(element, max_depth=3, depth=0, results=None):
if results is None: results = []
if depth > max_depth: return results
if get_attribute(element, 'AXRole') == 'AXButton':
results.append(element)
else:
for child in get_attribute(element, 'AXChildren') or []:
find_button(child, max_depth, depth+1, results)
return results
# BAD: Sequential blocking
for app in apps: windows.extend(get_windows(app))
# GOOD: Concurrent with ThreadPoolExecutor
async def get_all_windows_async():
with ThreadPoolExecutor(max_workers=4) as executor:
tasks = [loop.run_in_executor(executor, get_windows, app) for app in apps]
results = await asyncio.gather(*tasks)
return [w for wins in results for w in wins]
# BAD: Multiple calls
title = AXUIElementCopyAttributeValue(element, 'AXTitle', None)
role = AXUIElementCopyAttributeValue(element, 'AXRole', None)
# GOOD: Batch query
error, values = AXUIElementCopyMultipleAttributeValues(
element, ['AXTitle', 'AXRole', 'AXPosition', 'AXSize'], None
)
info = dict(zip(attributes, values)) if error == kAXErrorSuccess else {}
# BAD: Observer for every notification without debounce
# GOOD: Selective observers with debouncing
class OptimizedObserver:
def __init__(self, app_element, notifications):
self.last_callback, self.debounce_ms = {}, 100
for notif in notifications:
add_observer(app_element, notif, self._debounced_callback)
def _debounced_callback(self, notification, element):
now = time() * 1000
if now - self.last_callback.get(notification, 0) < self.debounce_ms:
return
self.last_callback[notification] = now
self._handle_notification(notification, element)
| CVE/CWE | Severity | Description | Mitigation |
|---|---|---|---|
| CVE-2023-32364 | CRITICAL | TCC bypass via symlinks | Update macOS, validate paths |
| CVE-2023-28206 | HIGH | AX privilege escalation | Process validation, code signing |
| CWE-290 | HIGH | Bundle ID spoofing | Verify code signature |
| CWE-74 | HIGH | Input injection via AX | Block SecurityAgent |
| CVE-2022-42796 | MEDIUM | Hardened runtime bypass | Verify target app runtime |
| OWASP | Risk | Mitigation |
|---|---|---|
| A01 Broken Access | CRITICAL | TCC validation, blocklists |
| A02 Misconfiguration | HIGH | Minimal permissions |
| A05 Injection | HIGH | Input validation |
| A07 Auth Failures | HIGH | Code signature verification |
| Tier | Attributes | Actions | Timeout |
|---|---|---|---|
| read-only | AXTitle, AXRole, AXChildren | None | 30s |
| standard | All | AXPress, AXIncrement | 60s |
| elevated | All | All (except SecurityAgent) | 120s |
Critical Anti-Patterns - Always avoid:
pytest tests/ -vpytest -k "security or permission"pytest --cov --cov-fail-under=80Your goal is to create macOS accessibility automation that is:
Security Reminders :
references/advanced-patterns.mdreferences/security-examples.mdreferences/threat-model.mdWeekly Installs
161
Repository
GitHub Stars
29
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
gemini-cli136
opencode136
codex136
github-copilot125
cursor123
amp104
GitHub Actions 官方文档查询助手 - 精准解答 CI/CD 工作流问题
33,800 周安装