Skip to content

NorthwoodsCommunityChurch/Purchase-Assistant

Repository files navigation

Security Review: Purchase Assistant

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)


Executive Summary

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


Findings

PA-01: Process Execution for DOCX Zip Creation (MEDIUM)

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.


PA-02: Inbox JSON Parsing Without Schema Validation (LOW)

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.


PA-03: SwiftData Database Unencrypted (LOW)

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.


PA-04: Exported projects.json Readable by All Local Processes (LOW)

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.


PA-05: BOM Files Written to App Support (INFO)

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.


PA-06: HTML Content in WKWebView for PDF Generation (INFO)

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.


PA-07: Preferences in UserDefaults (INFO)

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.


Positive Observations

  • 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 Process invocation (/usr/bin/zip) uses argument arrays, not shell strings.
  • Proper HTML escaping: BOMHTMLRenderer.escapeHTML() and BOMDocxBuilder.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 ProjectExporter and BOMExporter use 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.

Risk Summary

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

About

macOS price tracking and comparison app for Northwoods purchasing

Topics

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors