feat(documents): add PAPERLESS_EXTERNAL_URL for browser links (#411)#412
Conversation
…links Add PAPERLESS_EXTERNAL_URL environment variable to support network setups where the server accesses Paperless-ngx via an internal URL but browsers need an external URL (e.g., PAPERLESS_URL=http://paperless:8000 internally, PAPERLESS_EXTERNAL_URL=https://paperless.example.com for browsers). Changes: - Add paperlessExternalUrl to AppConfig interface - Parse and validate PAPERLESS_EXTERNAL_URL in loadConfig() - Update /api/paperless/status to return external URL when set, falling back to PAPERLESS_URL - Update JSDoc for PaperlessStatusResponse.paperlessUrl - Add PAPERLESS_EXTERNAL_URL to CLAUDE.md environment variables table - Update docker-compose.yml with example configuration - Update docs/src/guides/documents/setup.md with network examples - Update docs/src/getting-started/configuration.md environment table Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
…AL_URL support Tests cover configuration parsing and validation: - PAPERLESS_EXTERNAL_URL not set → undefined - Valid https:// and http:// URLs accepted - Invalid schemes (file://, ftp://) rejected with clear error messages - Invalid URL strings rejected with validation errors - Empty string treated as undefined (consistent with other env vars) - External URL can be set independently of internal URL - External URL takes precedence over internal URL in API responses - Backward compatibility: no external URL falls back to internal URL Integration tests verify API response behavior: - GET /api/paperless/status returns external URL when configured - Status endpoint returns external URL even when Paperless unreachable - Backward compatibility: internal URL used when no external URL configured Fixes #411 Co-Authored-By: Claude qa-integration-tester (Haiku 4.5) <noreply@anthropic.com>
steilerDev
left a comment
There was a problem hiding this comment.
[security-engineer] Security review of PR #412 — PAPERLESS_EXTERNAL_URL env var.
Summary
Reviewed the 9-file diff focused on SSRF prevention and env var validation. The implementation is clean and follows the established validation pattern for PAPERLESS_URL.
Security Analysis
SSRF Prevention — PASS
The critical security invariant is correctly maintained: PAPERLESS_EXTERNAL_URL is only used for browser-facing links; it never replaces paperlessUrl (the internal API proxy target) in any server-side fetch call.
requirePaperless()inpaperless.tsreturnsfastify.config.paperlessUrlfor all proxy endpoints — unchanged.- Only
GET /api/paperless/statususespaperlessExternalUrl, and only in the response body for the client to use as anhref. - The external URL never flows into
paperlessServicefetch calls.
URL Validation — PASS
PAPERLESS_EXTERNAL_URL receives identical scheme validation to PAPERLESS_URL:
new URL()parse with try/catch for malformed input- Allowlist:
['http:', 'https:']— blocksfile://,ftp://,javascript:, etc. - Empty string treated as
undefined(correct behavior, no crash) - Validation errors accumulate in the shared
errors[]array and throw together
Tests cover: https, http, file, ftp, invalid string, empty, standalone (no PAPERLESS_URL), and all-three-set scenarios.
Frontend URL Rendering — PASS
paperlessBaseUrl from the status response is used in two places:
DocumentDetailPanel.tsx:19— ``${paperlessBaseUrl}/documents/${document.id}/details```LinkedDocumentCard.tsx:85— same pattern
Both render as href on <a target="_blank" rel="noopener noreferrer"> elements. Since the scheme is enforced server-side to http:/https: only, javascript: injection is not possible. The rel="noopener noreferrer attribute is already present.
The pre-existing open finding #17 from PR #400 (client-side javascript: URL guard) does not apply here — the URL originates from a server-validated config field, not from user-supplied data.
No New Attack Surface
- No user-controlled input reaches this code path.
- Config-time validation (startup fail-fast) prevents a misconfigured instance from serving bad links.
paperlessExternalUrlis not logged in the startup log (acceptable — it is a non-sensitive URL, but omitting it avoids any confusion with the internal URL).
Verdict
No security issues found. The implementation correctly separates browser-facing URLs from server-side proxy targets and applies the same validated scheme allowlisting already used for PAPERLESS_URL.
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect]
Architecture Review: feat(documents): add PAPERLESS_EXTERNAL_URL for browser links (#411)
Verdict: Approve (posted as comment -- cannot approve own-repo PRs)
This is a clean, well-scoped enhancement. The approach of reusing the existing paperlessUrl field in PaperlessStatusResponse to always represent the browser-facing URL is the correct design -- it avoids adding a new field, avoids frontend changes, and maintains backward compatibility.
What was verified
Config plugin (server/src/plugins/config.ts)
paperlessExternalUrladded toAppConfiginterface -- correct placement- URL validation mirrors
PAPERLESS_URLvalidation exactly (scheme allowlist, URL parsing, error collection) -- consistent - Empty string treated as undefined via existing
getValue()helper -- consistent with other env vars paperlessEnabledremains gated onpaperlessUrl + paperlessApiTokenonly --PAPERLESS_EXTERNAL_URLalone does not enable the integration (AC 8 satisfied)
Route handler (server/src/routes/paperless.ts)
- Line 126:
paperlessExternalUrl ?? paperlessUrl ?? null-- correct fallback chain - Only the
/statusendpoint is affected. All proxy endpoints inrequirePaperless()continue usingpaperlessUrlexclusively for server-to-server calls (AC 5 satisfied)
Shared types (shared/src/types/document.ts)
- JSDoc updated to clarify
paperlessUrlnow represents the browser-facing URL -- good contract documentation - No new fields added, no breaking changes
Tests
- 9 config validation tests covering: valid URLs, invalid schemes (file://, ftp://), invalid strings, empty string, independent of internal URL, combined with internal URL
- 3 integration tests covering: external URL returned when reachable, external URL returned when unreachable, backward compatibility fallback
- Test teardown properly cleans up
process.env.PAPERLESS_EXTERNAL_URL
Documentation
CLAUDE.mdenv var table updateddocker-compose.ymlexample config updated with inline comments distinguishing internal vs external- Docs site: configuration reference and setup guide both updated with Docker network examples
Findings
-
LOW -- Wiki not updated: The API-Contract.md wiki has an environment variables table (line 4389-4392) and a
GET /api/paperless/statusresponse shape (lines 4408-4434) that do not includePAPERLESS_EXTERNAL_URLor thepaperlessUrlresponse field. The Architecture.md wiki Paperless-ngx section (lines 250-303) also lacksPAPERLESS_EXTERNAL_URL. These should be updated for wiki accuracy. The status response examples in the wiki have been missing thepaperlessUrlfield since EPIC-08 -- this PR is a good opportunity to fix both. -
INFO -- Config logging: The config plugin startup log (line 197) logs
paperlessUrlbut notpaperlessExternalUrl. Not blocking, but logging it would help operators verify their configuration on startup.
Both findings are non-blocking. The implementation is architecturally sound, backward-compatible, well-tested, and properly documented in the source tree.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
🎉 This PR is included in version 1.12.0-beta.15 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 1.12.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
PAPERLESS_EXTERNAL_URLoptional env var for deployments where the server accesses Paperless-ngx via an internal URL but browsers need an external onePAPERLESS_EXTERNAL_URL(when set) aspaperlessUrlinGET /api/paperless/status, falling back toPAPERLESS_URLdocker-compose.yml, andCLAUDE.mdFixes #411
Test plan
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com