Skip to content

fix(exports): gate /export/record and /export/history with require_local - #2383

Closed
sedatdagg wants to merge 1 commit into
debpalash:mainfrom
sedatdagg:fix/export-endpoints-require-loopback
Closed

sedatdagg wants to merge 1 commit into
debpalash:mainfrom
sedatdagg:fix/export-endpoints-require-loopback

Conversation

@sedatdagg

@sedatdagg sedatdagg commented Sep 28, 2026 •

Copy link
Copy Markdown

POST /export/record and GET /export/history were the only two routes on the
export router with no access dependency. On a desktop install both auth
middlewares are inert — NetworkAccessMiddleware needs a share PIN and
BearerKeyMiddleware needs OMNIVOICE_API_KEY, and a desktop install sets
neither — so nothing else stood in the way: any host on the same network could
forge rows into the user's export history and read it back. The rows carry
absolute destination paths, so the GET also leaked the shape of the user's
filesystem.
Found by auditing the router's guard matrix against its siblings; no issue filed.
This revision replaces the require_loopback first attempt with require_local
after @greptile-apps flagged (P1) that require_loopback would break remote
backends. Detail under Changes.
Changes
backend/api/routers/exports.py — dependencies=[Depends(require_local)] on
POST /export/record and GET /export/history.
tests/test_exports_api.py — regression coverage for all three deployment
shapes, plus a module-docstring entry describing it.
Why require_local and not require_loopback. require_loopback is too
strict here. Under OMNIVOICE_SERVER_MODE (Docker, or an Electron client
pointed at a remote backend) the bridge NAT rewrites client.host to the
gateway, so every caller looks non-loopback — POST is a non-safe method and
falls through to require_admin, which a share PIN cannot satisfy, and GET is
refused as soon as any credential is configured. The file would save and then
vanish from the user's history. require_local is the consumption-tier
companion api/dependencies.py documents for exactly this case.
Resulting behaviour, verified by executing the real require_local against each
shape:
Deployment
caller
before
after
Desktop (default)
loopback
allow
allow
Desktop (default)
LAN peer
allow — the bug
403
Desktop, OMNIVOICE_TRUSTED_NETWORKS set
listed LAN peer
allow
allow
Server mode (Docker / remote Electron)
bridge gateway
allow
allow
With OMNIVOICE_TRUSTED_NETWORKS unset — the desktop default — require_local
is identical to require_loopback, so the LAN exposure stays closed. In server
mode it is a no-op, so the remote deployment keeps working; exposure there stays
governed by the operator's port mapping plus the optional PIN or API key.
Export history is consumption data, not admin surface. The destructive
DELETE /export/history/{id} keeps its stricter require_loopback — untouched,
as is every other route on the router.
Type
[x] 🐛 Bug fix
[ ] ✨ New feature
[ ] ♻️ Refactor
[ ] 📝 Documentation
[ ] 🧪 Tests
[ ] 🔧 CI / Build
[ ] 🚀 Release prep
Testing
pytest could not be run in the authoring sandbox — it has no package index
access, so the backend dependencies cannot be installed. CI is the authority
on the suite. What was verified locally instead, by executing the real code
rather than a paraphrase of it:
Extracted require_local from backend/api/dependencies.py and
is_local_host / is_loopback / _trusted_networks from backend/core/auth.py
via ast.get_source_segment, executed them against stubbed collaborators, and
confirmed all eight scenarios in the table above (loopback v4/v6, three
non-loopback addresses, the Docker bridge gateway with and without server
mode, and an operator-trusted LAN range).
AST check of the router: the two routes now resolve to require_local,
POST /export, POST /export/reveal and DELETE /export/history/{id} are
unchanged, no route is left unguarded, and all three imported dependencies are
still referenced (no dead import).
Confirmed from backend/main.py that NetworkAccessMiddleware and
BearerKeyMiddleware are inert without a PIN / API key, so the new tests
exercise the dependency rather than a middleware rejection.
New tests (each parametrised over both routes):
test_history_routes_reject_non_local_callers — a 192.168.1.50 client gets
403. Fails against main, where the routes have no dependency at all.
test_history_routes_allow_loopback — the desktop app still gets 200.
test_history_routes_stay_reachable_in_server_mode — under
OMNIVOICE_SERVER_MODE=1 a non-loopback client gets 200. This is the
@greptile-apps P1 as an executable assertion: it fails against the
require_loopback revision of this branch.
Checklist
[x] I've tested this locally
[x] I've updated relevant documentation (if applicable) — module docstring of
tests/test_exports_api.py; no docs/** or README text describes these
routes' access tier, so nothing else is affected by the docs-sync rule
[x] No local machine paths, logs, or personal env details in this PR
[ ] Maintained version files are in sync (if an owner-requested bump) — n/a,
no version bump
[x] If this PR changes runtime behavior, the regression fixture at
tests/fixtures/omnivoice_data/ still loads green on the smoke-matrix
CI job (macOS + Windows + Linux) — no schema, data or path change; to be
confirmed by CI
Release cadence
VoiceStudio ships continuous-to-main — no release candidates, no soak windows.
Every merged PR is immediately part of rolling source (main) and Docker
:latest. Electron artifact rehearsals validate desktop packages without publishing.
Version bumps require owner approval; validated releases are tagged from main
and published explicitly under the release checklist.
Users who want stability install an Electron release or pin Docker :stable.

Unauthenticated callers on a LAN could inject export history records
or read local filesystem paths exposed in export history. Added
`dependencies=[Depends(require_loopback)]` to both endpoints, matching
the pattern already used by DELETE /export/history/{id}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CV9A88tyfnofgQu4Dyj16
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Restricts two export endpoints to loopback-only access.

The PR is not ready to merge because a supported remote-backend save can succeed without being recorded in export history, and the required guard tests are missing.

Fix All in Claude CodeFindings

  1. P1 Remote saves lose history ▶
  2. P2 Access guards lack regression tests ▶
Summary

Adds loopback dependencies to export-record creation and history listing.

  • A supported remote-backend save can lose its history record when no admin credential is configured.
  • The new access behavior lacks the regression coverage required for fixes.

Reviews (1) · Last reviewed commit: "fix: require_loopback on POST /export/re..."



@router.post("/export/record")
@router.post("/export/record", dependencies=[Depends(require_loopback)])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Remote saves lose history If Electron saves a file while connected to a remote backend without an admin credential, the new guard rejects the history-recording request with 403. The file is saved, but saveExport only logs the error, so the export never appears in history. Preserve an authorized way to record completed native saves without allowing unauthenticated remote writes.

Fix in Claude Code



@router.post("/export/record")
@router.post("/export/record", dependencies=[Depends(require_loopback)])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Access guards lack regression tests The new guards on /export/record and /export/history have no tests checking that non-loopback callers are rejected and authorized loopback callers succeed. CLAUDE.md requires a fail-before/pass-after regression test for fixes; add that coverage for both routes before merging.

Context Used: Review as a panel of senior domain experts (ML inference, audio DSP, desktop systems). Comment ONLY on findings that would change what gets merged: a concrete bug, a violated house rule from CLAUDE.md, a real security/data risk. Per finding: at most ... (source)

Fix in Claude Code

@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: debpalash/VoiceStudio/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 067f2ea9-aab1-4686-b7ac-5967a5986e40

📥 Commits

Reviewing files that changed from the base of the PR and between 08a1592 and f33ccae.

📒 Files selected for processing (1)
  • backend/api/routers/exports.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The /export/record and /export/history routes now require loopback access. Their handlers are unchanged.

Changes

Export route access

Layer / File(s) Summary
Apply loopback dependency
backend/api/routers/exports.py
Both export routes now apply require_loopback.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~5 minutes

Change: Bug fix

Suggested reviewers: debpalash

Merge Risk: ⚪ Minimal · up to f33cc

The export routes follow the existing server-mode access rules, with no identified issue blocking merge.

Architecture Summary

Architecture risk: 🔵 Low · up to f33cc

The change affects 1 system.

Changed systems: backend

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — backend (api) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in backend/api/routers/exports.py: The /export/record route now applies require_loopback; it previously had no route-level dependency.
  • observed — Modified behavior in backend/api/routers/exports.py: The /export/history route now applies require_loopback; it previously had no route-level dependency.
🚥 Pre-merge checks | ✅ 6 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the change but fails the required conventional-commit format because it has no scope. It also lacks an issue reference in the title or body. Use a scoped title such as "fix(exports): require_loopback on POST /export/record and GET /export/history" and add the required issue reference to the title or body.
Description check ⚠️ Warning The description includes a useful summary and change list but omits the required Type, Testing, and Checklist sections. It does not document test execution or checklist status. Add the missing template sections, select the applicable change type, document testing, and complete the checklist. Include the Release cadence section if the repository requires the full template.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cross-Platform Default Parity ✅ Passed The default behavior is platform-neutral. Both changed routes call the same require_loopback dependency; is_loopback accepts the same 127.0.0.1, ::1, and localhost values on macOS, Windows, …
I18n Completeness (21 Locales) ✅ Passed The pull request changes only backend/api/routers/exports.py. The diff adds route dependencies and changes no frontend code, t('...') keys, or user-facing frontend strings.
Local-First Guarantee ✅ Passed The PR changes only two route declarations in backend/api/routers/exports.py. It adds the existing local require_loopback dependency to /export/record and /export/history; it adds no cloud cal…
Backward Compatibility ✅ Passed The PR changes only two route dependency declarations in backend/api/routers/exports.py. The diff contains no database schema, Alembic migration, voice, project, settings, engine, or model-weight ch…
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

debpalash added a commit that referenced this pull request Sep 29, 2026
Consolidate community engine, workflow, dictation, and setup fixes with the Electron composer, sidebar, and voice UI.

Fix review findings in engine residency, remote exports, backup cleanup, bounded compressed-audio decoding, and reference preprocessing. Preserve contributor credits in CHANGELOG.md and leave the app version unchanged.

Supersedes #2325, #2338, #2368, #2377, #2379, #2380, #2383, #2384, #2387, #2390, #2391, #2392, #2393, #2395, #2400, #2401, #2402, #2409, #2410, and #2412.
@debpalash

Copy link
Copy Markdown
Owner

Implemented and merged through #2419. Contributor credit is preserved in CHANGELOG.md.

@debpalash debpalash closed this Sep 29, 2026
@sedatdagg sedatdagg changed the title fix: require_loopback on POST /export/record and GET /export/history fix(exports): gate /export/record and /export/history with require_local Oct 1, 2026
@sedatdagg

Copy link
Copy Markdown
Author

Superseded by #2545 — this PR stopped tracking its head branch and stayed at the initial commit, so the pushed review fixes never appeared here. #2545 is the same branch with the Greptile P1 addressed (require_local instead of require_loopback), regression tests, and docstring coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants