Skip to main content

max / goingson

Frontend/Security: fix duplicate import key, validate checkout URL, align version Ultra-fuzz Run #30 Phase 4/5 (Frontend/Security polish). - F1: api.js declared `import:` twice; the second (vCard/iCal) block shadowed the first, so api.import.preview/.execute were undefined and the native CSV import UI was broken at runtime. Merge both into one `import:` object. - S3: route the SyncKit checkout URL through the same is_external_http_url scheme validator as open_external_url before handing it to the OS opener, so a compromised sync server can't return a file:// (etc.) URL. Made the validator pub(crate). - Align workspace Cargo.toml version 0.4.0 -> 0.4.2 to match tauri.conf.json (reconciliation only, no release). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-22 04:03 UTC
Commit: 9f5795aff89f25053c1cf73d55cd44289f36c686
Parent: 2d95f1f
5 files changed, +20 insertions, -15 deletions
M Cargo.lock +2 -2
@@ -1836,7 +1836,7 @@ dependencies = [
1836 1836
1837 1837 [[package]]
1838 1838 name = "goingson-core"
1839 - version = "0.4.0"
1839 + version = "0.4.2"
1840 1840 dependencies = [
1841 1841 "async-trait",
1842 1842 "chrono",
@@ -1854,7 +1854,7 @@ dependencies = [
1854 1854
1855 1855 [[package]]
1856 1856 name = "goingson-db-sqlite"
1857 - version = "0.4.0"
1857 + version = "0.4.2"
1858 1858 dependencies = [
1859 1859 "argon2",
1860 1860 "async-trait",
M Cargo.toml +1 -1
@@ -8,7 +8,7 @@ default-members = ["src-tauri"]
8 8 resolver = "2"
9 9
10 10 [workspace.package]
11 - version = "0.4.0"
11 + version = "0.4.2"
12 12 edition = "2024"
13 13 license-file = "LICENSE"
14 14
@@ -329,10 +329,17 @@ const api = {
329 329 },
330 330
331 331 // Import — native CSV/TSV import (entity type auto-detected from headers)
332 + // plus vCard/iCalendar import. Must be ONE object: a second `import:` key
333 + // would shadow this one and silently break whichever block lost the merge
334 + // (it previously broke the native CSV preview/execute path).
332 335 import: {
333 336 preview: (filePath, options = {}) => invoke('preview_import', { input: { filePath, options } }), // Dry-run parse
334 337 execute: (filePath, options = {}, selectedIndices = []) =>
335 338 invoke('execute_import', { input: { filePath, options, selectedIndices } }), // Create entities in DB
339 + previewVcf: (filePath) => invoke('preview_vcf', { filePath }),
340 + importVcf: (filePath) => invoke('import_vcf', { filePath }),
341 + previewIcs: (filePath) => invoke('preview_ics', { filePath }),
342 + importIcs: (filePath) => invoke('import_ics', { filePath }),
336 343 },
337 344
338 345 // Attachments — file attachments on tasks and projects
@@ -348,14 +355,6 @@ const api = {
348 355 fileSize: (filePath) => invoke('get_file_size', { filePath }),
349 356 },
350 357
351 - // External Import — vCard contacts and iCalendar events
352 - import: {
353 - previewVcf: (filePath) => invoke('preview_vcf', { filePath }),
354 - importVcf: (filePath) => invoke('import_vcf', { filePath }),
355 - previewIcs: (filePath) => invoke('preview_ics', { filePath }),
356 - importIcs: (filePath) => invoke('import_ics', { filePath }),
357 - },
358 -
359 358 // Time Tracking — start/stop timer, sessions, summaries
360 359 timeTracking: {
361 360 startTimer: (taskId) => invoke('start_timer', { taskId }),
@@ -432,9 +432,15 @@ pub async fn sync_subscribe(
432 432 }
433 433 };
434 434
435 - // Open in default browser
436 - if let Err(e) = open::that(&response.checkout_url) {
437 - tracing::warn!(error = %e, "Failed to open browser, returning URL");
435 + // Open in default browser. Route through the same scheme validator as
436 + // open_external_url so a malicious/compromised sync server can't return a
437 + // non-http(s) URL (e.g. file://) that the OS opener would act on.
438 + if crate::commands::window::is_external_http_url(&response.checkout_url) {
439 + if let Err(e) = open::that(&response.checkout_url) {
440 + tracing::warn!(error = %e, "Failed to open browser, returning URL");
441 + }
442 + } else {
443 + tracing::warn!("Refusing to open non-http(s) checkout URL");
438 444 }
439 445
440 446 Ok(response.checkout_url)
@@ -159,7 +159,7 @@ pub async fn open_external_url(
159 159 /// Whether `url` is an `http`/`https` URL safe to hand to the system opener.
160 160 /// Rejects every other scheme (`file:`, `javascript:`, `data:`, custom-handler
161 161 /// schemes) and scheme-relative or path inputs.
162 - fn is_external_http_url(url: &str) -> bool {
162 + pub(crate) fn is_external_http_url(url: &str) -> bool {
163 163 let lower = url.to_ascii_lowercase();
164 164 lower.starts_with("https://") || lower.starts_with("http://")
165 165 }