fix(kon): security audit fixes — CSP, XSS, unwraps, key rename
Security fixes from code audit:
- CSP re-enabled in tauri.conf.json with strict directives
(was null — critical vulnerability)
- XSS fix in viewer highlightText(): HTML entities escaped before
inserting <mark> tags via {@html}
- Removed 3 unwrap() calls in rule_based.rs British English conversion
— replaced with safe let-else guards
- Removed unwrap() on main window lookup in lib.rs setup — now uses
if-let for graceful handling
- Wrapped JSON.parse in DictationPage transcription-result listener
with try/catch
Rebrand cleanup:
- Renamed all localStorage keys from ramble_* to kon_* across
7 files (stores, viewer, float, history)
12 tests passing, clippy clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,10 @@ pub fn to_british_english(text: &str) -> String {
|
|||||||
if let Ok(re) = regex_lite::Regex::new(&pattern) {
|
if let Ok(re) = regex_lite::Regex::new(&pattern) {
|
||||||
result = re
|
result = re
|
||||||
.replace_all(&result, |caps: ®ex_lite::Captures| {
|
.replace_all(&result, |caps: ®ex_lite::Captures| {
|
||||||
let matched = caps.get(0).unwrap().as_str();
|
let Some(m) = caps.get(0) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
let matched = m.as_str();
|
||||||
let us_base = us.replace("\\b", "");
|
let us_base = us.replace("\\b", "");
|
||||||
let base_len = us_base.len();
|
let base_len = us_base.len();
|
||||||
let suffix = if matched.len() > base_len {
|
let suffix = if matched.len() > base_len {
|
||||||
@@ -121,12 +124,17 @@ pub fn to_british_english(text: &str) -> String {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let first_char = matched.chars().next().unwrap();
|
let Some(first_char) = matched.chars().next() else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
let uk_clean = uk.replace("\\b", "");
|
let uk_clean = uk.replace("\\b", "");
|
||||||
if first_char.is_uppercase() {
|
if first_char.is_uppercase() {
|
||||||
let mut chars = uk_clean.chars();
|
let mut chars = uk_clean.chars();
|
||||||
|
let Some(first_uk) = chars.next() else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
let upper_first: String =
|
let upper_first: String =
|
||||||
chars.next().unwrap().to_uppercase().collect();
|
first_uk.to_uppercase().collect();
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
upper_first,
|
upper_first,
|
||||||
|
|||||||
@@ -34,15 +34,17 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close-to-tray: hide window instead of exiting
|
// Close-to-tray: hide window instead of exiting
|
||||||
let main_window = app.get_webview_window("main").unwrap();
|
if let Some(main_window) = app.get_webview_window("main") {
|
||||||
let win = main_window.clone();
|
let win = main_window.clone();
|
||||||
main_window.on_window_event(move |event| {
|
main_window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event
|
if let tauri::WindowEvent::CloseRequested { api, .. } =
|
||||||
{
|
event
|
||||||
api.prevent_close();
|
{
|
||||||
let _ = win.hide();
|
api.prevent_close();
|
||||||
}
|
let _ = win.hide();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost; connect-src ipc: http://ipc.localhost asset: https://asset.localhost http://localhost:* ws://localhost:*; media-src 'self' asset: https://asset.localhost"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -47,9 +47,15 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
unlisten = await listen("transcription-result", (event) => {
|
unlisten = await listen("transcription-result", (event) => {
|
||||||
const result = typeof event.payload === "string"
|
let result;
|
||||||
? JSON.parse(event.payload)
|
try {
|
||||||
: event.payload;
|
result = typeof event.payload === "string"
|
||||||
|
? JSON.parse(event.payload)
|
||||||
|
: event.payload;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse transcription result:", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -129,11 +129,11 @@
|
|||||||
async function openViewer(item) {
|
async function openViewer(item) {
|
||||||
// Store item data for the viewer window
|
// Store item data for the viewer window
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("ramble_viewer_item", JSON.stringify(item));
|
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
|
||||||
await invoke("open_viewer_window");
|
await invoke("open_viewer_window");
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback: open in browser tab (dev mode)
|
// Fallback: open in browser tab (dev mode)
|
||||||
localStorage.setItem("ramble_viewer_item", JSON.stringify(item));
|
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
|
||||||
window.open("/viewer", "_blank", "width=600,height=700");
|
window.open("/viewer", "_blank", "width=600,height=700");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const page = $state({
|
|||||||
|
|
||||||
// ---- Settings (persisted to localStorage) ----
|
// ---- Settings (persisted to localStorage) ----
|
||||||
|
|
||||||
const SETTINGS_KEY = "ramble_settings";
|
const SETTINGS_KEY = "kon_settings";
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
engine: "whisper",
|
engine: "whisper",
|
||||||
@@ -53,7 +53,7 @@ export function saveSettings() {
|
|||||||
|
|
||||||
// ---- Profiles (persisted to localStorage) ----
|
// ---- Profiles (persisted to localStorage) ----
|
||||||
|
|
||||||
const PROFILES_KEY = "ramble_profiles";
|
const PROFILES_KEY = "kon_profiles";
|
||||||
|
|
||||||
function loadProfiles() {
|
function loadProfiles() {
|
||||||
try {
|
try {
|
||||||
@@ -73,7 +73,7 @@ export function saveProfiles() {
|
|||||||
|
|
||||||
// ---- History (persisted to localStorage) ----
|
// ---- History (persisted to localStorage) ----
|
||||||
|
|
||||||
const HISTORY_KEY = "ramble_history";
|
const HISTORY_KEY = "kon_history";
|
||||||
|
|
||||||
function loadHistory() {
|
function loadHistory() {
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +104,7 @@ export function deleteFromHistory(index) {
|
|||||||
|
|
||||||
// ---- Tasks (persisted to localStorage) ----
|
// ---- Tasks (persisted to localStorage) ----
|
||||||
|
|
||||||
const TASKS_KEY = "ramble_tasks";
|
const TASKS_KEY = "kon_tasks";
|
||||||
|
|
||||||
function loadTasks() {
|
function loadTasks() {
|
||||||
try {
|
try {
|
||||||
@@ -169,7 +169,7 @@ export function uncompleteTask(id) {
|
|||||||
// ---- BroadcastChannel for multi-window task sync ----
|
// ---- BroadcastChannel for multi-window task sync ----
|
||||||
|
|
||||||
const taskChannel = typeof BroadcastChannel !== "undefined"
|
const taskChannel = typeof BroadcastChannel !== "undefined"
|
||||||
? new BroadcastChannel("ramble_tasks")
|
? new BroadcastChannel("kon_tasks")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (taskChannel) {
|
if (taskChannel) {
|
||||||
@@ -189,7 +189,7 @@ function broadcastTasks() {
|
|||||||
|
|
||||||
// ---- Task Lists (persisted to localStorage) ----
|
// ---- Task Lists (persisted to localStorage) ----
|
||||||
|
|
||||||
const TASK_LISTS_KEY = "ramble_task_lists";
|
const TASK_LISTS_KEY = "kon_task_lists";
|
||||||
|
|
||||||
function loadTaskLists() {
|
function loadTaskLists() {
|
||||||
try {
|
try {
|
||||||
@@ -309,7 +309,7 @@ export function sortTaskLists(mode) {
|
|||||||
|
|
||||||
// BroadcastChannel for task lists
|
// BroadcastChannel for task lists
|
||||||
const taskListChannel = typeof BroadcastChannel !== "undefined"
|
const taskListChannel = typeof BroadcastChannel !== "undefined"
|
||||||
? new BroadcastChannel("ramble_task_lists")
|
? new BroadcastChannel("kon_task_lists")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (taskListChannel) {
|
if (taskListChannel) {
|
||||||
@@ -329,7 +329,7 @@ function broadcastTaskLists() {
|
|||||||
|
|
||||||
// ---- Templates (persisted to localStorage) ----
|
// ---- Templates (persisted to localStorage) ----
|
||||||
|
|
||||||
const TEMPLATES_KEY = "ramble_templates";
|
const TEMPLATES_KEY = "kon_templates";
|
||||||
|
|
||||||
function loadTemplates() {
|
function loadTemplates() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
// Listen for settings changes from main window
|
// Listen for settings changes from main window
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("storage", (e) => {
|
window.addEventListener("storage", (e) => {
|
||||||
if (e.key === "ramble_settings" && e.newValue) {
|
if (e.key === "kon_settings" && e.newValue) {
|
||||||
try {
|
try {
|
||||||
Object.assign(settings, JSON.parse(e.newValue));
|
Object.assign(settings, JSON.parse(e.newValue));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
// Sync settings from main window
|
// Sync settings from main window
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("storage", (e) => {
|
window.addEventListener("storage", (e) => {
|
||||||
if (e.key === "ramble_settings" && e.newValue) {
|
if (e.key === "kon_settings" && e.newValue) {
|
||||||
try { Object.assign(settings, JSON.parse(e.newValue)); } catch {}
|
try { Object.assign(settings, JSON.parse(e.newValue)); } catch {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
// Load item data from localStorage (set by HistoryPage before opening this window)
|
// Load item data from localStorage (set by HistoryPage before opening this window)
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("ramble_viewer_item");
|
const raw = localStorage.getItem("kon_viewer_item");
|
||||||
if (raw) {
|
if (raw) {
|
||||||
item = JSON.parse(raw);
|
item = JSON.parse(raw);
|
||||||
if (item.audioPath) {
|
if (item.audioPath) {
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleStorageChange(e) {
|
function handleStorageChange(e) {
|
||||||
if (e.key === "ramble_viewer_item" && e.newValue) {
|
if (e.key === "kon_viewer_item" && e.newValue) {
|
||||||
try {
|
try {
|
||||||
if (audioEl) { audioEl.pause(); }
|
if (audioEl) { audioEl.pause(); }
|
||||||
cancelAnimationFrame(animFrameId);
|
cancelAnimationFrame(animFrameId);
|
||||||
@@ -150,12 +150,13 @@
|
|||||||
|
|
||||||
let matchCount = $derived.by(() => matchingSegments.size);
|
let matchCount = $derived.by(() => matchingSegments.size);
|
||||||
|
|
||||||
// Highlight search matches in text
|
// Highlight search matches in text (XSS-safe: escapes HTML entities first)
|
||||||
function highlightText(text) {
|
function highlightText(text) {
|
||||||
if (!searchQuery.trim()) return text;
|
if (!searchQuery.trim()) return text;
|
||||||
|
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
const q = searchQuery.trim();
|
const q = searchQuery.trim();
|
||||||
const regex = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
const regex = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
||||||
return text.replace(regex, '<mark class="bg-accent/25 text-text rounded px-0.5">$1</mark>');
|
return escaped.replace(regex, '<mark class="bg-accent/25 text-text rounded px-0.5">$1</mark>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Segment editing
|
// Segment editing
|
||||||
@@ -200,17 +201,17 @@
|
|||||||
function saveItemToHistory() {
|
function saveItemToHistory() {
|
||||||
// Persist edits back to localStorage history
|
// Persist edits back to localStorage history
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("ramble_history");
|
const raw = localStorage.getItem("kon_history");
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const hist = JSON.parse(raw);
|
const hist = JSON.parse(raw);
|
||||||
const idx = hist.findIndex((h) => h.id === item.id);
|
const idx = hist.findIndex((h) => h.id === item.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
hist[idx] = { ...hist[idx], segments: item.segments, text: item.text };
|
hist[idx] = { ...hist[idx], segments: item.segments, text: item.text };
|
||||||
localStorage.setItem("ramble_history", JSON.stringify(hist));
|
localStorage.setItem("kon_history", JSON.stringify(hist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Also update the viewer item cache
|
// Also update the viewer item cache
|
||||||
localStorage.setItem("ramble_viewer_item", JSON.stringify(item));
|
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user