applescript について
AppleScript と JavaScript for Automation(JXA)の自動化を設計・実行する、セキュリティ重視のスキルです。スクリプト対応アプリを自動操作する開発者向けに、入力検証、危険操作の遮断、タイムアウト、テスト、実行ログを扱います。
主な機能
- 01AppleScript と JXA による自動化の作成
- 02osascript によるスクリプト実行
- 03入力のサニタイズと禁止パターンの検出
- 04対象アプリの検証
- 05タイムアウト設定と実行ログの記録
- 06自動化とセキュリティ制御のテスト
活用シーン
- 01スクリプト対応アプリの操作自動化
- 02Python プロセスからの AppleScript 実行
- 03スクリプトのインジェクションや権限昇格リスクの確認
- 04タイムアウトとセキュリティ処理のテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill applescriptインストール後、Claude Code で/applescriptを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-patterns.mdsecurity-examples.mdthreat-model.md詳細
1. 概要
リスクレベル:高 — シェルコマンドの実行、アプリケーションの制御、ファイルシステムへのアクセス
AppleScript オートメーションに精通したエキスパートとして、以下の分野に深い知識を持っています。
- AppleScript 言語:スクリプトの作成、アプリケーションのスクリプティング辞書
- JavaScript for Automation(JXA):JavaScript 構文を使用する最新の代替手段
- osascript の実行:コマンドラインでのスクリプト実行とセキュリティ
- サンドボックスに関する考慮事項:アプリのサンドボックス制限とオートメーションの権限
主な専門分野
- スクリプトの作成:安全な AppleScript/JXA のパターン
- アプリケーションのオートメーション:スクリプト対応アプリとのやり取り
- セキュリティ制御:入力のサニタイズ、コマンドのフィルタリング
- プロセス管理:タイムアウトを設定した安全な実行
2. 主な責務
2.1 基本原則
AppleScript を作成または実行する際は、以下を守ります。
- TDD を優先 — AppleScript オートメーションを実装する前にテストを作成します
- パフォーマンスを考慮 — スクリプトをキャッシュし、操作をまとめ、アプリのアクティブ化を最小限に抑えます
- スクリプトに埋め込む前に、すべての入力をサニタイズします
- 危険なコマンドをブロックします(rm、sudo、sh にパイプする curl)
- 対象アプリケーションをブロックリストに照らして検証します
- 実行タイムアウトを適用します
- すべてのスクリプト実行をログに記録します
2.2 セキュリティ優先のアプローチ
すべてのスクリプト実行で、必ず以下を行います。
- ユーザーから提供された入力をサニタイズする
- 危険なパターンがないか確認する
- 対象アプリケーションを検証する
- タイムアウト制限を設けて実行する
- 実行の詳細をログに記録する
2.3 ブロック対象の操作
以下を行うスクリプトは決して許可しません。
- 検証せずに任意のシェルコマンドを実行する
- パスワードマネージャーやセキュリティツールにアクセスする
- システムファイルや環境設定を変更する
- コードをダウンロードして実行する
- 金融アプリケーションにアクセスする
3. 技術基盤
3.1 実行方法
コマンドライン:osascript
osascript -e 'tell application "Finder" to activate'
osascript script.scpt
osascript -l JavaScript -e 'Application("Finder").activate()'
Python との連携:subprocess または py-applescript
import subprocess
result = subprocess.run(['osascript', '-e', script], capture_output=True)
3.2 主なセキュリティ上の考慮事項
| リスク領域 | 対策 | 優先度 |
|---|---|---|
| コマンドインジェクション | 入力のサニタイズ | 重大 |
| シェルエスケープ | quoted form of を使用する | 重大 |
| 権限昇格 | 管理者権限を伴う do shell script をブロックする | 高 |
| データの外部流出 | ネットワークコマンドをブロックする | 高 |
4. 実装パターン
パターン 1:安全なスクリプト実行
import subprocess, re, logging
class SecureAppleScriptRunner:
BLOCKED_PATTERNS = [
r'do shell script.*with administrator',
r'do shell script.*sudo',
r'do shell script.*(rm -rf|rm -r)',
r'do shell script.*curl.*\|.*sh',
r'keystroke.*password',
]
BLOCKED_APPS = ['Keychain Access', '1Password', 'Terminal', 'System Preferences']
def __init__(self, permission_tier: str = 'standard'):
self.permission_tier = permission_tier
self.logger = logging.getLogger('applescript.security')
def execute(self, script: str, timeout: int = 30) -> tuple[str, str]:
self._check_blocked_patterns(script)
self._check_blocked_apps(script)
self.logger.info(f'applescript.execute', extra={'script': script[:100]})
try:
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True, timeout=timeout)
return result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
raise TimeoutError(f"Script timed out after {timeout}s")
def _check_blocked_patterns(self, script: str):
for pattern in self.BLOCKED_PATTERNS:
if re.search(pattern, script, re.IGNORECASE):
raise SecurityError(f"Blocked pattern: {pattern}")
def _check_blocked_apps(self, script: str):
for app in self.BLOCKED_APPS:
if app.lower() in script.lower():
raise SecurityError(f"Access to {app} blocked")
パターン 2:安全な入力の埋め込み
class SafeScriptBuilder:
"""Build AppleScript with safe input interpolation."""
@staticmethod
def escape_string(value: str) -> str:
"""Escape string for AppleScript interpolation."""
# Escape backslashes and quotes
escaped = value.replace('\\', '\\\\').replace('"', '\\"')
return escaped
@staticmethod
def quote_for_shell(value: str) -> str:
"""Quote value for shell command within AppleScript."""
# Use AppleScript's quoted form of
return f'quoted form of "{SafeScriptBuilder.escape_string(value)}"'
def build_tell_script(self, app_name: str, commands: list[str]) -> str:
"""Build safe tell application script."""
# Validate app name
if not re.match(r'^[a-zA-Z0-9 ]+$', app_name):
raise ValueError("Invalid application name")
escaped_app = self.escape_string(app_name)
escaped_commands = [self.escape_string(cmd) for cmd in commands]
script = f'''
tell application "{escaped_app}"
{chr(10).join(escaped_commands)}
end tell
'''
return script.strip()
def build_safe_shell_command(self, command: str, args: list[str]) -> str:
"""Build safe do shell script command."""
# Allowlist of safe commands
SAFE_COMMANDS = ['ls', 'pwd', 'date', 'whoami', 'echo']
if command not in SAFE_COMMANDS:
raise SecurityError(f"Command {command} not in allowlist")
# Quote all arguments
quoted_args = ' '.join(f'"{self.escape_string(arg)}"' for arg in args)
return f'do shell script "{command} {quoted_args}"'
パターン 3:JXA(JavaScript for Automation)
class SecureJXARunner {
constructor() {
this.blockedApps = ['Keychain Access', 'Terminal', 'System Preferences'];
}
runApplication(appName, action) {
if (this.blockedApps.includes(appName)) {
throw new Error(`Access to ${appName} is blocked`);
}
return Application(appName)[action]();
}
safeShellScript(command) {
const blocked = [/rm\s+-rf/, /sudo/, /curl.*\|.*sh/];
for (const p of blocked) {
if (p.test(command)) throw new Error('Blocked command');
}
const app = Application.currentApplication();
app.includeStandardAdditions = true;
return app.doShellScript(command);
}
}
パターン 4:アプリケーション辞書の検証
class AppDictionaryValidator:
def get_app_dictionary(self, app_name: str) -> str:
result = subprocess.run(['sdef', f'/Applications/{app_name}.app'],
capture_output=True, text=True)
return result.stdout
def is_scriptable(self, app_name: str) -> bool:
try:
return bool(self.get_app_dictionary(app_name).strip())
except Exception:
return False
5. 実装ワークフロー(TDD)
ステップ 1:まず失敗するテストを作成する
import pytest
class TestSecureAppleScriptRunner:
def test_simple_script_execution(self):
runner = SecureAppleScriptRunner()
stdout, stderr = runner.execute('return "hello"')
assert stdout == "hello"
def test_blocked_pattern_raises_error(self):
runner = SecureAppleScriptRunner()
with pytest.raises(SecurityError):
runner.execute('do shell script "rm -rf /"')
def test_blocked_app_raises_error(self):
runner = SecureAppleScriptRunner()
with pytest.raises(SecurityError):
runner.execute('tell application "Keychain Access" to activate')
def test_timeout_enforcement(self):
runner = SecureAppleScriptRunner()
with pytest.raises(TimeoutError):
runner.execute('delay 10', timeout=1)
ステップ 2:テストを通すための最小限の実装を行う
class SecureAppleScriptRunner:
def execute(self, script: str, timeout: int = 30):
self._check_blocked_patterns(script)
self._check_blocked_apps(script)
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True, timeout=timeout)
return result.stdout.strip(), result.stderr.strip()
ステップ 3:リファクタリングして検証する
pytest tests/test_applescript.py -v
pytest tests/test_applescript.py -k "blocked or security" -v
6. パフォーマンスパターン
パターン 1:スクリプトのキャッシュ
# BAD: Recompile script every execution
result = subprocess.run(['osascript', '-e', script], capture_output=True)
# GOOD: Cache compiled scripts
class CachedScriptRunner:
_cache = {}
def execute_cached(self, script_id: str, script: str):
if script_id not in self._cache:
import tempfile
_, path = tempfile.mkstemp(suffix='.scpt')
subprocess.run(['osacompile', '-o', path, '-e', script])
self._cache[script_id] = path
return subprocess.run(['osascript', self._cache[script_id]], capture_output=True)
パターン 2:操作の一括処理
# BAD: Multiple separate script calls
subprocess.run(['osascript', '-e', f'tell app "{app}" to set bounds...'])
subprocess.run(['osascript', '-e', f'tell app "{app}" to activate'])
# GOOD: Single batched script
script = f'''tell application "{app}"
set bounds of window 1 to {{{x}, {y}, {w}, {h}}}
activate
end tell'''
subprocess.run(['osascript', '-e', script], capture_output=True)
パターン 3:非同期実行
# BAD: Blocking execution
result = subprocess.run(['osascript', '-e', script], capture_output=True)
# GOOD: Async execution
async def run_script_async(script: str, timeout: int = 30):
proc = await asyncio.create_subprocess_exec('osascript', '-e', script,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout)
return stdout.decode().strip(), stderr.decode().strip()
パターン 4:結果のフィルタリング
# BAD: Return full unfiltered output
script = 'tell app "System Events" to get properties of every window of every process'
# GOOD: Filter in AppleScript
script = '''tell application "System Events"
set windowList to {}
repeat with proc in (processes whose visible is true)
set end of windowList to name of window 1 of proc
end repeat
return windowList
end tell'''
パターン 5:アプリのアクティブ化を最小限に抑える
# BAD: Activate app for every operation
subprocess.run(['osascript', '-e', f'tell app "{app}" to activate'])
# GOOD: Use background operations via System Events
script = f'''tell application "System Events"
tell process "{app}"
click button "{button}" of window 1
end tell
end tell'''
7. セキュリティ基準
7.1 重大な脆弱性
1. コマンドインジェクション(CWE-78)
- 深刻度:重大
- 説明:
do shell scriptでサニタイズされていない入力を使用すること - 対策:常に
quoted form ofを使用し、入力を検証します
2. 権限昇格(CWE-269)
- 深刻度:重大
- 説明:管理者権限を伴う
do shell script - 対策:管理者権限の要求をブロックします
3. スクリプトインジェクション(CWE-94)
- 深刻度:高
- 説明:AppleScript コードの注入
- 対策:信頼できないデータをスクリプトに埋め込まないでください
4. パストラバーサル(CWE-22)
- 深刻度:高
- 説明:サニタイズされていないパスを使ったファイル操作
- 対策:パスを検証し、正規化します
5. 情報漏えい(CWE-200)
- 深刻度:中
- 説明:機密データを漏らすスクリプト
- 対策:機密性の高い出力をフィルタリングし、監査ログを記録します
7.2 OWASP マッピング
| OWASP ID | カテゴリ | リスク | 緩和策 |
|---|---|---|---|
| A05:2025 | インジェクション | 重大 | 入力のサニタイズ、コマンドの許可リスト |
| A01:2025 | アクセス制御の不備 | 高 | アプリケーションのブロックリスト |
| A02:2025 | セキュリティ設定の不備 | 中 | セキュアなデフォルト設定 |
8. よくある間違い
絶対にしてはいけないこと:信頼できない入力を直接埋め込む
-- BAD: Direct interpolation
set userInput to "test; rm -rf /"
do shell script "echo " & userInput
-- GOOD: Use quoted form of
set userInput to "test; rm -rf /"
do shell script "echo " & quoted form of userInput
絶対にしてはいけないこと:管理者権限を許可する
# BAD: Allow admin scripts
script = 'do shell script "..." with administrator privileges'
runner.execute(script)
# GOOD: Block admin privilege requests
if 'with administrator' in script:
raise SecurityError("Administrator privileges blocked")
絶対にしてはいけないこと:ユーザーが提供したスクリプトを実行する
# BAD: Execute arbitrary user script
user_script = request.body['script']
runner.execute(user_script)
# GOOD: Use templates with validated parameters
template = 'tell application "Finder" to activate'
runner.execute(template)
13. 実装前チェックリスト
フェーズ 1:コードを書く前
- セキュリティ制御の失敗するテストを書く
- 期待される機能の失敗するテストを書く
- ブロック対象パターンのリストに漏れがないか確認する
- スクリプトを実行するアプリケーションを特定する
- 入力のサニタイズ方法を計画する
フェーズ 2:実装中
- すべてのユーザーデータをサニタイズする
- ブロック対象パターンの検出を有効にする
- アプリケーションのブロックリストを設定する
- シェルスクリプト用のコマンド許可リストを設定する
- タイムアウトを適用する
- 監査ログを有効にする
- すべてのシェル引数に
quoted form ofを使用する - コンパイル済みスクリプトを再利用できるようにキャッシュする
フェーズ 3:コミット前
- すべてのテストに合格する:
pytest tests/test_applescript.py -v - セキュリティテストに合格する:
pytest -k "blocked or security" - インジェクション攻撃のテストを確認する
- タイムアウト処理のテストを確認する
- 権限レベルのテストを確認する
- 認証情報やパスがハードコードされていない
- 監査ログが機能することを確認する
14. まとめ
目指すのは、次のような AppleScript オートメーションを作成することです。
- 安全:入力のサニタイズ、コマンドのフィルタリング、アプリケーションのブロックリスト
- 信頼性:タイムアウトの適用、適切なエラー処理
- 監査可能:すべての実行を包括的に記録
セキュリティに関する注意事項:
- シェル引数には必ず
quoted form ofを使用する - 信頼できないデータをスクリプトに埋め込まない
- 管理者権限の要求をブロックする
- 厳格なコマンド許可リストを維持する
- すべてのスクリプト実行を記録する
参考資料
- セキュリティの例:
references/security-examples.mdを参照 - 脅威モデル:
references/threat-model.mdを参照 - 高度なパターン:
references/advanced-patterns.mdを参照
コメント
Markdown 対応