Skip to main content

Security

Tusab runs locally on the user's machine — a privacy advantage, but that doesn't eliminate risk. Three attack surfaces exist even in a local desktop app: the FastAPI API on localhost:8001, user input (URLs, uploads, pasted text), and the React interface running inside Electron.

What was already secure from the start

  • Electron with best practicescontextIsolation: true, nodeIntegration: false, sandbox: true, explicit preload script
  • Safe subprocesses — yt-dlp is always executed via an argument list, never shell=True (prevents shell injection)
  • Sensitive files in .gitignorecredentials.json, token.json, .env, agent_config.json have never been committed
  • File name sanitization — regex strips special characters before saving uploads to disk

Audit and fixes applied

Two rounds of end-to-end code auditing (June 2026) covered subprocess injection, path traversal, input validation, Electron security, secret storage, CORS, XSS, and dependencies. Twelve findings identified and fixed:

#FixSeverity
1CORS restricted to localhost:8001 (was: allow_origins=["*"])Critical
2yt-dlp playlist ID validated by regex before composing commandsCritical
3Upload limited to 50 MB before processingHigh
4File delete protected against path traversal (os.path.realpath())High
5dangerouslySetInnerHTML eliminated from ReactMedium
6API keys migrated to the OS keychain via safeStorageMedium
7Size limits on every Pydantic field (max_length)Low
8Path traversal in the static file server fixedHigh
9Prompt injection mitigated with XML delimiters in the RAG promptMedium
10Google Drive query injection fixed with quote escapingMedium
11YouTube URL validated against a regex whitelistLow
12Chat history moved to the server — client payload ignoredLow

Example — CORS

Before: allow_origins=["*"], allow_credentials=True — any site open in the user's browser, or any local process, could make unrestricted requests to the API.

After: allow_origins=["http://localhost:8001", "http://127.0.0.1:8001"], allow_credentials=False — only the app itself can call the API.

Example — prompt injection in the RAG pipeline

The prompt assembles each component with semantic XML delimiters:

<source id="1">
<title>...</title>
<date>...</date>
<link>...</link>
<content>...</content>
</source>

<conversation_history>...</conversation_history>

<question>...</question>

The model receives an explicit structure: content inside <source> is treated as reference data, not as instruction. The question is capped at 2,000 characters.

Example — path traversal on delete

real_path = os.path.realpath(txt_path)
real_subdir = os.path.realpath(subdir)
if not real_path.startswith(real_subdir + os.sep):
return {"error": True, "message": "Invalid path"}

os.path.realpath() resolves the real absolute path — including any ../ or symlink — and verifies it's actually inside the allowed folder before any delete operation.

General posture

  • No central server to attack or compromise
  • Data never leaves the machine without explicit consent
  • Electron configured with isolation best practices
  • The 12 fixes close the realistic attack surfaces for the current distribution phase

What's still missing for an enterprise posture: local authentication token for the API (server mode), pinned dependency versions, automated tests covering critical security endpoints.

For personal data handling (LGPD/GDPR), see Privacy.