Review Date: 2026-03-01 Reviewer: Alice (automated security review agent) App Version: Current development (pre-release, no git remote) Platform: macOS (Swift / SwiftUI / SwiftData)
Purchase Assistant is a low-to-moderate risk macOS app for tracking purchase prices. It uses SwiftData for local persistence, a file-system inbox for JSON import, and generates BOM documents (PDF via WKWebView, DOCX via XML + zip). The app has no network communication, no authentication, no credential storage, and no self-update mechanism. The primary security considerations relate to local file handling: the inbox watcher processes JSON files from a known directory, and the DOCX builder shells out to /usr/bin/zip. The overall attack surface is minimal since all data is local and the app has no external API calls.
Overall Risk Level: LOW
| Field | Value |
|---|---|
| Severity | MEDIUM |
| Category | Code Injection |
| CWE | CWE-78 (Improper Neutralization of Special Elements in OS Command) |
| File | Sources/Services/BOMDocxBuilder.swift:861-877 |
Description: The DOCX builder creates a zip archive by spawning /usr/bin/zip via Process(). The output path is derived from the project name via sanitizedForFilename(), which replaces filesystem-unsafe characters. The zip command is:
process.arguments = ["-r", "-q", outputURL.path, "."]The outputURL.path comes from project.name.sanitizedForFilename() + " - BOM.docx". Since arguments are passed as an array (not a shell string), there is no shell injection risk. However, sanitizedForFilename() only strips /:\?%*|"<> -- it does not strip single quotes, backticks, spaces, or dollar signs from project names.
Risk Assessment: Low practical risk because Process.arguments passes values directly to execve, bypassing shell interpretation. The project name is user-controlled local input, not external. The currentDirectoryURL is set to a temp directory with a UUID name, limiting path confusion.
Recommendation: The current implementation is safe due to argument-array execution. No immediate action needed. If the execution model ever changes to shell-based invocation, the sanitization would need to be expanded.
| Field | Value |
|---|---|
| Severity | LOW |
| Category | Input Validation |
| CWE | CWE-20 (Improper Input Validation) |
| File | Sources/Services/InboxImporter.swift:7-10 |
Description: The inbox importer reads JSON files from ~/Library/Application Support/Purchase Assistant/inbox/ and decodes them as InboxPayload using JSONDecoder. There is no validation of field lengths, no size limit on the JSON file, and no restriction on the number of results or prices per result.
Risk Assessment: The inbox directory is only writable by the current user (standard macOS permissions). The JSON comes from Claude Code sessions running on the same machine. Malformed JSON simply fails Codable decoding and is caught by the try/catch. An extremely large JSON file could cause memory pressure but this is a local-only scenario.
Recommendation: No immediate action needed. The trust boundary is the local user account. If the inbox is ever made accessible to external sources, add file size limits and field length validation.
| Field | Value |
|---|---|
| Severity | LOW |
| Category | Data Storage |
| CWE | CWE-312 (Cleartext Storage of Sensitive Information) |
| File | Sources/App/PurchaseAssistantApp.swift:13 |
Description: The SwiftData model container stores all project data (item names, prices, retailer URLs, recommendations) in an unencrypted SQLite database in the app's container directory.
Risk Assessment: The data stored is purchasing research information (product names, prices, retailer names, URLs). This is not sensitive personal information. No credentials, API keys, or authentication tokens are stored. The database is protected by macOS file permissions.
Recommendation: No action needed for current use case. If the app ever stores financial account information or credentials, consider encrypting the database.
| Field | Value |
|---|---|
| Severity | LOW |
| Category | Data Storage |
| CWE | CWE-732 (Incorrect Permission Assignment for Critical Resource) |
| File | Sources/Services/ProjectExporter.swift:32 |
Description: The projects.json export file is written to ~/Library/Application Support/Purchase Assistant/ with default file permissions. The file is written using Data.write(to:options:.atomic), which creates files with the user's default umask (typically 0644, readable by all local users).
Risk Assessment: The exported data contains product names, prices, and retailer information -- not sensitive personal data. The file is intentionally designed to be read by Claude Code sessions. On a single-user Mac (typical for this use case), there is no meaningful risk.
Recommendation: No action needed for current deployment. If multi-user Mac support is needed, consider restricting file permissions to 0600.
| Field | Value |
|---|---|
| Severity | INFO |
| Category | Data Storage |
| File | Sources/Services/BOMDocumentExporter.swift:6-8 |
Description: BOM documents (PDF and DOCX) are automatically generated and written to ~/Library/Application Support/Purchase Assistant/bom/. These contain project and pricing data in human-readable format. Files are cleaned up when projects are deleted.
Risk Assessment: The BOM directory contains the same data as the SwiftData database, just in document format. Standard macOS file permissions apply. The cleanup logic (cleanupOrphanedFiles) removes files for deleted projects.
| Field | Value |
|---|---|
| Severity | INFO |
| Category | Code Injection |
| CWE | CWE-79 (Cross-site Scripting) |
| File | Sources/Services/BOMHTMLRenderer.swift:32, 801-807 |
Description: The BOM PDF generator builds HTML from project data and loads it into a hidden WKWebView. The escapeHTML() function properly escapes &, <, >, and " in all user-supplied values (project names, item names, recommendations, etc.). The HTML is loaded with baseURL: nil, preventing access to local files.
Risk Assessment: The HTML escaping is correctly implemented. The WKWebView is hidden (not user-facing) and is used purely for PDF rendering. Even if escaping were bypassed, the WKWebView has no access to cookies, storage, or network resources since it has no base URL.
Positive observation: Good use of escapeHTML() on all interpolated values.
| Field | Value |
|---|---|
| Severity | INFO |
| Category | Data Storage |
| File | Sources/App/AppPreferences.swift |
Description: User preferences (preferred retailer name, preferred brand names) are stored in UserDefaults. These are non-sensitive configuration values.
Risk Assessment: No security concern. The stored values are retailer/brand name strings, not credentials or personal data.
- No network communication: The app makes zero network requests. All data exchange happens via local JSON files.
- No credential storage: No API keys, passwords, tokens, or secrets anywhere in the codebase.
- No self-update mechanism: No trampoline scripts, no binary downloads, no osascript usage.
- No shell injection risk: The only
Processinvocation (/usr/bin/zip) uses argument arrays, not shell strings. - Proper HTML escaping:
BOMHTMLRenderer.escapeHTML()andBOMDocxBuilder.esc()both properly escape user input for their respective output formats. - Proper XML escaping:
BOMDocxBuilder.esc()handles&,<,>,", and'for DOCX XML generation. - Proper CSV escaping:
BOMExporter.csvEscape()handles commas, quotes, and newlines per RFC 4180. - File sanitization:
sanitizedForFilename()strips common filesystem-unsafe characters from project names before use in file paths. - Atomic file writes: Both
ProjectExporterandBOMExporteruse atomic writes to prevent partial file corruption. - Codable type safety: JSON import uses strongly-typed Codable structs, preventing arbitrary data injection.
- SwiftData cascade deletes: Relationship delete rules prevent orphaned records.
| ID | Finding | Severity |
|---|---|---|
| PA-01 | Process execution for zip | MEDIUM |
| PA-02 | Inbox JSON no schema validation | LOW |
| PA-03 | SwiftData unencrypted | LOW |
| PA-04 | projects.json default permissions | LOW |
| PA-05 | BOM files in app support | INFO |
| PA-06 | HTML in WKWebView for PDF | INFO |
| PA-07 | Preferences in UserDefaults | INFO |
Critical: 0 | High: 0 | Medium: 1 | Low: 3 | Info: 3