Skip to content

feat(paperless): implement Paperless-ngx proxy service and routes#362

Merged
steilerDev merged 3 commits into
betafrom
feat/354-paperless-proxy-service
Mar 2, 2026
Merged

feat(paperless): implement Paperless-ngx proxy service and routes#362
steilerDev merged 3 commits into
betafrom
feat/354-paperless-proxy-service

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Add paperlessService.ts — HTTP client service that proxies all requests to Paperless-ngx, resolves tag/correspondent/document type IDs to names, and handles binary passthrough for thumbnails and previews
  • Add paperless.ts route plugin — 6 authenticated endpoints under /api/paperless/* (status, document list/detail, thumb, preview, tags) with 503 when not configured and 502 on upstream errors
  • Register paperless routes in app.ts and add commented Paperless env var examples to docker-compose.yml
  • 73 tests: 42 service unit tests (fetch mock) + 31 route integration tests (app.inject)

Fixes #354

Implementation Details

  • Uses Node.js built-in fetch (no additional HTTP client dependency)
  • All requests include Accept: application/json; version=5 per ADR-015
  • API token stays server-side; browser never sees it
  • N+1 optimization: unique correspondent/document type IDs resolved once per list request
  • Binary passthrough uses arrayBuffer() and forwards content-type and content-disposition headers from upstream

Test plan

  • 42 unit tests pass (paperlessService.test.ts)
  • 31 integration tests pass (paperless.test.ts)
  • Pre-commit hook quality gates pass (lint, format, typecheck, build, audit)
  • All endpoints return 401 when unauthenticated
  • All proxy endpoints return 503 PAPERLESS_NOT_CONFIGURED when env vars missing
  • Status endpoint always returns 200 OK (not 503)
  • Binary endpoints forward content-type from upstream

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com

Add the Paperless-ngx proxy backend for EPIC-08. The server now proxies
all document browsing and binary requests through /api/paperless/* endpoints,
keeping the API token server-side at all times.

Fixes #354

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[product-owner]

Requirements Coverage Review for Story 8.1 (#354)

I have systematically verified each acceptance criterion against the PR diff. The implementation is thorough and well-structured, with strong test coverage (73 tests). However, there are two gaps that need to be addressed before I can accept this story.

Acceptance Criteria Evaluation

# Criterion Verdict Notes
1 Accepts PAPERLESS_URL and PAPERLESS_TOKEN env vars PASS Implementation uses PAPERLESS_API_TOKEN (not PAPERLESS_TOKEN), which is consistent with the architecture (ADR-015, API Contract, CLAUDE.md). The issue text used shorthand; the architecture is the source of truth.
2 Fastify plugin registers API client with token auth PASS paperlessService.ts uses Authorization: Token <token> header. Routes registered as a Fastify plugin via app.register(paperlessRoutes) in app.ts.
3 GET /api/paperless/status returns connection status and Paperless-ngx instance version PARTIAL FAIL The endpoint correctly returns { configured, reachable, error }, which matches the API Contract. However, the acceptance criterion explicitly calls for "the Paperless-ngx instance version" in the response. The API Contract and shared types do not include a version field. See finding below.
4 GET /api/paperless/documents with search, tag filtering, and pagination PASS Supports query, tags (comma-separated IDs), correspondent, documentType, page, pageSize, sortBy, sortOrder. Pagination metadata returned.
5 GET /api/paperless/documents/:id returns single document metadata PASS Returns title, created date, correspondent (resolved name), document type (resolved name), tags, content.
6 GET /api/paperless/documents/:id/thumbnail proxies thumbnail image PASS Implemented at /documents/:id/thumb (matching the API Contract and ADR-015 specification of thumb). Binary passthrough with content-type forwarding.
7 GET /api/paperless/documents/:id/preview proxies document preview PASS Binary passthrough with content-type and content-disposition forwarding.
8 GET /api/paperless/tags returns tag list PASS Returns all tags with id, name, color (mapped from Paperless colour IDs), and documentCount.
9 All proxy endpoints require authentication PASS Every route handler checks request.user and throws UnauthorizedError(). Integration tests verify 401 for all endpoints.
10 Not configured returns 503 PAPERLESS_NOT_CONFIGURED PASS requirePaperless() helper throws 503 PAPERLESS_NOT_CONFIGURED when paperlessEnabled is false. Status endpoint returns 200 with configured: false instead (correct per API Contract).
11 Unreachable/error returns 502 without leaking internals PASS Network errors produce PAPERLESS_UNREACHABLE (502), upstream non-ok responses produce PAPERLESS_ERROR (502). Error messages describe the failure category without exposing internal details.
12 .env.example updated with new variables FAIL The .env.example file was NOT updated. Only docker-compose.yml was updated with commented examples. The criterion and the issue notes both specify .env.example should be updated.

Findings Requiring Changes

1. (Medium) .env.example not updated

The .env.example file at the repo root has no mention of PAPERLESS_URL or PAPERLESS_API_TOKEN. Acceptance criterion 12 explicitly requires this. Please add the variables (commented out) to .env.example, similar to how the OIDC variables are documented there:

# ─── Paperless-ngx (optional) ─────────────────────────
# PAPERLESS_URL=http://paperless:8000
# PAPERLESS_API_TOKEN=your-paperless-api-token

2. (Low) Status endpoint does not return Paperless-ngx instance version

Acceptance criterion 3 states the status endpoint should return "the Paperless-ngx instance version". The current implementation returns { configured, reachable, error } with no version field. The API Contract and shared PaperlessStatusResponse type also lack a version field. This appears to be a deliberate architectural decision (the status probe hits /api/documents/?page_size=1 rather than a version-specific endpoint).

I am classifying this as low severity because the API Contract is the authoritative design, and the implementation matches it. However, the acceptance criterion text is not fully satisfied. The resolution should be one of:

  • (a) Update the status endpoint to also retrieve and return the Paperless-ngx instance version (e.g., from its /api/ root endpoint), OR
  • (b) Update the acceptance criterion on issue #354 to remove the version requirement, acknowledging the architecture deliberately omits it.

Either path is acceptable; please pick one.

Positive Observations

  • Strong test coverage: 42 unit tests + 31 integration tests covering happy paths, error paths, auth enforcement, and edge cases
  • N+1 optimization for correspondent/document type resolution is well-implemented
  • Binary passthrough correctly forwards content-type and content-disposition headers
  • API version pinning (version=5) per ADR-015
  • Clean separation between service layer and route handlers

Verdict: REQUEST CHANGES

Two items must be resolved before this story can be accepted. Finding 1 is a concrete gap (missing file update). Finding 2 needs a decision on which artifact to align (implementation or acceptance criterion).

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[security-engineer]

Security Review — PR #362: Paperless-ngx Proxy Service Backend Foundation

Verdict: Request Changes — 1 Medium finding requires attention before merge. All other findings are Low/Informational.

Note: GitHub prevents self-review, so posting as a comment. The findings below carry the same weight as a formal request-changes review.


Summary

The implementation is architecturally sound from a security standpoint. The Paperless-ngx API token is properly kept server-side and never returned to the browser. All 6 endpoints enforce session authentication correctly — the preValidation hook in auth.ts enforces auth globally, and each handler also explicitly checks request.user (belt and suspenders). Query parameters are constructed via URLSearchParams, preventing URL injection. The binary passthrough path is appropriately limited.

One Medium finding requires a fix: the PAPERLESS_URL environment variable is accepted without any format or scheme validation, creating an SSRF vector. Five additional Low/Informational findings are documented below.


[MEDIUM] SSRF via Unvalidated PAPERLESS_URL Environment Variable

OWASP Category: A10 — Server-Side Request Forgery (SSRF)
Severity: Medium

Description:
PAPERLESS_URL is accepted as a raw string with no URL validation in config.ts. Any string value (including internal network addresses, file paths, or scheme variants like file://, ftp://, or http://169.254.169.254/) is accepted without check and later concatenated directly into all upstream fetch URLs:

// paperlessService.ts
response = await fetch(`${baseUrl}${path}`, { headers: makeHeaders(token) });

This means an operator with .env file access can redirect all proxy requests to internal metadata services, cloud IMDS endpoints, or other internal services. In a typical self-hosted Docker Compose setup this is an operator-level concern (not an end-user concern), but it still represents an uncontrolled SSRF surface that should be bounded.

Affected Files:

  • server/src/plugins/config.ts:108paperlessUrl accepted without URL validation
  • server/src/services/paperlessService.ts:91fetch(`${baseUrl}${path}`, ...) — unvalidated base URL

Proof of Concept:
Set PAPERLESS_URL=http://169.254.169.254/latest/meta-data/ in .env. Any authenticated Cornerstone user triggering GET /api/paperless/status will cause the server to fetch from the AWS IMDS endpoint. The response is not returned to the user but proves the SSRF path. Setting PAPERLESS_URL=file:///etc/passwd would depend on Node.js fetch behavior (likely rejected, but not guaranteed across runtimes).

Remediation:
Add URL validation in loadConfig using the built-in URL constructor with an allowlist of permitted schemes:

// In loadConfig():
if (paperlessUrl) {
  try {
    const parsed = new URL(paperlessUrl);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      errors.push(`PAPERLESS_URL must use http or https scheme, got: ${parsed.protocol}`);
    }
  } catch {
    errors.push(`PAPERLESS_URL is not a valid URL: ${paperlessUrl}`);
  }
}

This ensures only http:// and https:// URLs are accepted at startup, preventing file/ftp/data scheme SSRF and malformed URL injection.

Risk if Unaddressed:
An operator with .env write access could redirect proxy traffic to internal network services. In this self-hosted deployment model the attacker must already have server access, which limits blast radius significantly — but this finding still violates SSRF hygiene and should be fixed.


[LOW] getStatus Error Field Leaks Internal Connectivity Errors to Authenticated Clients

OWASP Category: A05 — Security Misconfiguration (information exposure)

Description:
When Paperless-ngx is configured but unreachable, getStatus() returns the raw exception message in the error field of the 200 response:

// paperlessService.ts:262-263
const message = err instanceof Error ? err.message : String(err);
return { configured: true, reachable: false, error: message };

This means network-level error strings like ECONNREFUSED connect ECONNREFUSED 10.0.0.5:8000, DNS resolution failures, or TLS certificate errors (which may include internal hostnames or IP addresses) are returned in the API response body to any authenticated user.

Affected Files:

  • server/src/services/paperlessService.ts:262-263

Remediation:
Sanitize or categorize the error before returning it. The simplest approach is to return a fixed string like "Cannot connect to Paperless-ngx" without the internal network detail. Alternatively, document this as accepted risk — in a self-hosted admin tool, surfacing connectivity details to authenticated users is a reasonable UX tradeoff.


[LOW] Binary Content-Type Passthrough Allows Arbitrary MIME Types

OWASP Category: A05 — Security Misconfiguration
Severity: Low

Description:
The thumb and preview endpoints forward the upstream content-type header from Paperless-ngx without any validation:

// paperless.ts:167, 204
const contentType = upstream.headers.get('content-type') ?? 'image/webp';
reply.header('content-type', contentType);

If Paperless-ngx returns an unexpected content-type (e.g., text/html, application/javascript, or image/svg+xml), Cornerstone will faithfully forward it. An image/svg+xml document containing embedded <script> tags could be executed by the browser if rendered inline, which would constitute a stored XSS vector depending on how the frontend displays the preview.

Affected Files:

  • server/src/routes/paperless.ts:165-173 — thumb endpoint, unvalidated content-type passthrough
  • server/src/routes/paperless.ts:202-210 — preview endpoint, unvalidated content-type passthrough

Remediation:
Allowlist the permitted content types and override to a safe fallback if the upstream returns something unexpected:

const ALLOWED_THUMB_TYPES = new Set(['image/webp', 'image/jpeg', 'image/png', 'image/gif']);
const ALLOWED_PREVIEW_TYPES = new Set(['application/pdf', 'image/tiff', 'image/jpeg', 'image/png']);

// For thumb:
const upstreamType = upstream.headers.get('content-type') ?? '';
const contentType = ALLOWED_THUMB_TYPES.has(upstreamType) ? upstreamType : 'image/webp';

// For preview:
const upstreamType = upstream.headers.get('content-type') ?? '';
const contentType = ALLOWED_PREVIEW_TYPES.has(upstreamType) ? upstreamType : 'application/pdf';

Additionally, set X-Content-Type-Options: nosniff on binary responses (this is also an open general finding for the application).

Risk if Unaddressed:
Low in practice — Paperless-ngx is a trusted backend. If the Paperless instance were compromised or misconfigured, a malicious SVG served through the thumb endpoint could execute JavaScript in the Cornerstone frontend's origin.


[LOW] tags Query Parameter Lacks Format Validation

OWASP Category: A03 — Injection (low severity)
Severity: Low

Description:
The tags query parameter is typed as { type: 'string', maxLength: 200 }. Any string up to 200 characters is accepted and forwarded to Paperless-ngx. URLSearchParams.set() correctly URL-encodes the value, so there is no HTTP header injection risk. However, non-numeric values are forwarded to Paperless-ngx and surface as PAPERLESS_ERROR 502, which may expose information about the upstream API error message.

Affected Files:

  • server/src/routes/paperless.ts:27tags: { type: 'string', maxLength: 200 } lacks pattern validation

Remediation:
Add a pattern constraint to enforce the expected comma-separated integer format:

tags: { type: 'string', maxLength: 200, pattern: '^\\d+(,\\d+)*$' },

[INFORMATIONAL] Positive Finding: Token Isolation

The Paperless-ngx API token is correctly kept server-side at all times. It is:

  • Not returned in any API response body
  • Excluded from the startup config log (config.ts:144-161)
  • Not passed through route handlers to the frontend
  • Stored only in environment variables, not in the database

This is the correct implementation pattern. No action needed.


[INFORMATIONAL] Positive Finding: URL Parameter Construction

All Paperless-ngx query strings are built using URLSearchParams, which correctly percent-encodes all values. The sortBy field is passed through a sortFieldMap allowlist before use. The id path parameter is validated as an integer by the AJV schema before it reaches the service. These patterns collectively prevent URL injection against the Paperless-ngx upstream.


Review Checklist

  • No SQL/command injection vectors in new code
  • Authentication enforced on all 6 new endpoints
  • No Paperless API token exposed in responses, logs, or errors
  • User input validated with AJV schemas on document list endpoint
  • Integer type enforcement on :id path parameter
  • No new dependencies introduced (uses Node.js built-in fetch)
  • No hardcoded credentials or secrets
  • PAPERLESS_URL not validated for scheme/format — MEDIUM finding above
  • Binary content-type not allowlisted — LOW finding above
  • tags parameter has no format constraint — LOW finding above

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[product-architect]

Architecture Review: Story 8.1 — Paperless-ngx Proxy Service

Summary

This PR implements the Paperless-ngx proxy backend foundation as specified in the EPIC-08 API contract and ADR-015. I have verified the implementation against the wiki API Contract, ADR-015, existing route/service patterns, shared types, and the config plugin. All CI checks are passing.

API Contract Compliance

All 6 proxy endpoints match the wiki API Contract specification:

Endpoint Contract Match Notes
GET /api/paperless/status Yes Always returns 200 OK with {configured, reachable, error} shape
GET /api/paperless/documents Yes Pagination, filtering (query, tags, correspondent, documentType), sorting all match contract
GET /api/paperless/documents/:id Yes Returns {document: PaperlessDocument} with full content
GET /api/paperless/documents/:id/thumb Yes Binary passthrough with forwarded content-type, default image/webp
GET /api/paperless/documents/:id/preview Yes Binary passthrough with forwarded content-type, default application/pdf
GET /api/paperless/tags Yes Returns {tags: PaperlessTag[]} sorted by ID

Error codes match the contract: PAPERLESS_NOT_CONFIGURED (503), PAPERLESS_UNREACHABLE (502), PAPERLESS_ERROR (502), NOT_FOUND (404). The status endpoint correctly returns 200 in all cases (not 503 when unconfigured), per the contract.

ADR-015 Compliance

  • Server-side proxy pattern: Confirmed. API token stays server-side; browser never sees it.
  • API version 5: Confirmed. Accept: application/json; version=5 header is set via makeHeaders(). The fetchBinary() function correctly omits the Accept header for binary requests.
  • Environment variable configuration: Confirmed. PAPERLESS_URL and PAPERLESS_API_TOKEN are read by the config plugin; paperlessEnabled derived flag is used in routes.
  • No caching (Phase 1): Confirmed. Every request makes a live upstream call.
  • Curated endpoint subset: Confirmed. Only the 5 proxy endpoints + status are exposed.

Architecture & Pattern Compliance

  • Route structure: Follows the established Fastify plugin pattern (register with prefix in app.ts).
  • Authentication: Uses if (!request.user) throw new UnauthorizedError() consistent with all other routes.
  • Error handling: Uses AppError with typed error codes registered in @cornerstone/shared. Error codes are registered in shared/src/types/errors.ts.
  • Service layer separation: Clean separation between paperlessService.ts (HTTP client logic) and paperless.ts (route handlers). The service is pure (no Fastify dependency), routes are thin wrappers.
  • Config plugin integration: paperlessEnabled, paperlessUrl, paperlessApiToken properly declared in AppConfig interface and config plugin.
  • Shared types: All response types (PaperlessStatusResponse, PaperlessDocumentListResponse, PaperlessDocumentDetailResponse, PaperlessTagListResponse) are correctly imported from @cornerstone/shared.
  • N+1 optimization: Unique correspondent and document type IDs are collected and resolved once per list request, with parallel Promise.all resolution.

Test Coverage

73 tests total: 42 unit tests + 31 integration tests. Coverage appears comprehensive:

  • All 6 endpoints tested for auth (401), not-configured (503), success (200), and upstream errors (502)
  • Binary endpoints tested for content-type forwarding and fallback defaults
  • Service tests cover: network errors, 404 mapping, non-ok responses, pagination, sorting, filtering, search hits, N+1 optimization, tag color mapping, null/minimal document fields
  • expect.assertions() used correctly in try/catch error assertion tests

Code Quality

  • TypeScript strict mode compatible; no any types
  • ESM imports with .js extensions throughout
  • Proper use of type imports (import type)
  • camelCase for TypeScript, snake_case for Paperless-ngx raw interfaces
  • Fastify JSON schema validation on query parameters and URL params
  • additionalProperties: false on querystring schema prevents unexpected parameters

Finding (Low Severity)

Duplicate ListDocumentsQuery interface in service: paperlessService.ts (line 270) defines a local ListDocumentsQuery interface that duplicates PaperlessDocumentListQuery from @cornerstone/shared. The local version has a wider sortBy: string type vs. the shared type's strict union. The service should import and use PaperlessDocumentListQuery from @cornerstone/shared instead to maintain a single source of truth. This is non-blocking since the route-level Fastify schema validation already constrains the values, but it should be cleaned up in a follow-up commit.

Verdict

Approved. The implementation is well-structured, fully compliant with the API contract and ADR-015, follows established patterns, and has thorough test coverage. The duplicate interface finding is low severity and can be addressed in a follow-up.

- config.ts: validate PAPERLESS_URL scheme (http/https only) to prevent SSRF
  attacks via file://, ftp://, and other non-HTTP schemes
- paperless.ts: add MIME type allowlist for thumb/preview binary endpoints;
  disallowed upstream content-types fall back to application/octet-stream
- paperless.ts: add pattern constraint '^\\d+(,\\d+)*$' to tags query param
  to enforce comma-separated integer format
- paperlessService.ts: sanitize IP addresses and hostname:port from error
  messages in getStatus() to prevent information disclosure
- paperlessService.ts: remove duplicate local ListDocumentsQuery interface;
  use shared PaperlessDocumentListQuery instead
- .env.example: add PAPERLESS_URL and PAPERLESS_API_TOKEN (commented out)
- Tests updated: config.test.ts, paperless.test.ts, paperlessService.test.ts
  cover all new validation logic, sanitization, and allowlist behavior

Fixes #354

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[product-owner]

Re-Review (Round 2) -- Story 8.1 (#354)

Note: GitHub prevents self-review approval. This comment carries the same weight as a formal approval.

Both round 1 findings have been addressed.

Finding 1 (Medium): .env.example not updated -- RESOLVED

Commit 31921e6 adds a Paperless-ngx section to .env.example with PAPERLESS_URL and PAPERLESS_API_TOKEN commented out. The format matches the existing OIDC variable section. Acceptance criterion 12 is now satisfied.

Finding 2 (Low): Status endpoint missing version -- ACCEPTED

The API Contract and shared PaperlessStatusResponse type deliberately omit a version field. The implementation returns { configured, reachable, error }, which matches the architectural design exactly. Since the API Contract is the source of truth and the omission is intentional (the status probe hits /api/documents/?page_size=1 rather than a version endpoint), I accept this as a deliberate scope decision. I will update the acceptance criterion text on issue #354 to align with the actual design.

Acceptance Criteria Summary

# Criterion Verdict
1 PAPERLESS_URL and PAPERLESS_API_TOKEN env vars PASS
2 Fastify plugin with token auth PASS
3 GET /api/paperless/status returns connection status PASS
4 GET /api/paperless/documents with search/filter/pagination PASS
5 GET /api/paperless/documents/:id returns document metadata PASS
6 GET /api/paperless/documents/:id/thumb proxies thumbnail PASS
7 GET /api/paperless/documents/:id/preview proxies preview PASS
8 GET /api/paperless/tags returns tag list PASS
9 All proxy endpoints require authentication PASS
10 Not configured returns 503 PAPERLESS_NOT_CONFIGURED PASS
11 Unreachable returns 502 without leaking internals PASS
12 .env.example updated PASS

Additional Observations

The fix commit also addresses security findings (SSRF prevention via URL scheme validation, MIME type allowlisting, tags parameter pattern validation, error message sanitization) and an architecture finding (duplicate ListDocumentsQuery interface removed). These improvements strengthen the implementation beyond the original acceptance criteria.

CI checks (Quality Gates, Docker, E2E Smoke Tests) are all passing.

Verdict: APPROVED -- All 12 acceptance criteria are satisfied. Story 8.1 is accepted.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[security-engineer] Round 2 remediation verification — all four findings from the initial review are correctly resolved.

Verification Summary

[MEDIUM] SSRF via Unvalidated PAPERLESS_URL — RESOLVED

Fix location: server/src/plugins/config.ts (lines 138–157 in the diff)

The implementation is correct and complete:

  • new URL(paperlessUrlRaw) correctly rejects malformed URLs (e.g., not-a-url) with a caught TypeError
  • The allowedSchemes = ['http:', 'https:'] set rejects file:, ftp:, and any other non-HTTP scheme
  • The protocol is stripped of its trailing colon in the error message (parsed.protocol.replace(':', '')) — consistent with test expectations
  • Validation errors feed into the existing errors[] accumulator, so startup fails fast with all config errors collected
  • 7 tests cover: http acceptance, https acceptance, file:// rejection, ftp:// rejection, invalid URL rejection, and disabled-when-partial-config behavior

No issues found.


[LOW] Binary Content-Type Passthrough — RESOLVED

Fix location: server/src/routes/paperless.ts (BINARY_CONTENT_TYPE_ALLOWLIST + sanitizeBinaryContentType())

The implementation is correct:

  • Allowlist: { image/webp, image/png, image/jpeg, image/gif, application/pdf } — covers all expected Paperless-ngx output types
  • contentType.split(';')[0].trim().toLowerCase() correctly strips MIME parameters before matching (e.g., image/webp; charset=utf-8image/webp)
  • Disallowed types (including text/html, image/svg+xml, application/javascript) fall back to application/octet-stream — a safe, non-executable MIME type
  • Applied consistently to both /thumb (fallback: image/webp) and /preview (fallback: application/pdf)
  • content-disposition is forwarded as-is, which is acceptable — Node.js rejects CRLF header injection at the transport layer, and Paperless-ngx is a trusted configured upstream

No issues found.


[LOW] getStatus Error Information Disclosure — RESOLVED

Fix location: server/src/services/paperlessService.ts (sanitizeErrorMessage())

The implementation is correct:

  • Regex 1: IPv4 addresses with optional port (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}(?::\d+)?) → <host>
  • Regex 2: hostname:port patterns ([a-zA-Z0-9](?:[a-zA-Z0-9\-._]*[a-zA-Z0-9])?:\d{1,5}) → <host>
  • Verified that HTTP status text like "returned 502: Bad Gateway" is NOT incorrectly matched — the regex requires the colon to be directly adjacent to the hostname token, not preceded by a space
  • Error type information (e.g., ECONNREFUSED, ENOTFOUND) is preserved after sanitization
  • IPv6 addresses are not fully sanitized (e.g., ::1 would not be caught), but this is an edge case with no practical impact in typical Docker network configurations
  • Tests confirm IP address redaction, hostname:port redaction, and preservation of the error type

No new issues introduced.


[LOW] tags Parameter Missing Format Validation — RESOLVED

Fix location: server/src/routes/paperless.ts (listDocumentsSchema)

tags: { type: 'string', maxLength: 200, pattern: '^\\d+(,\\d+)*$' },

The pattern is correct:

  • ^\d+$ — single integer
  • ^(\d+)(,\d+)*$ — comma-separated integers with no trailing comma
  • Rejects whitespace, letters, SQL fragments, and other injection attempts at the Fastify schema layer (400 before reaching service code)
  • Tests confirm rejection of abc, 1 OR 1=1 (URL-decoded), and acceptance of 5,12,20

No issues found.


Security Checklist

  • No SQL/command/XSS injection vectors in new code
  • Authentication enforced on all new endpoints (explicit !request.user checks on all 6 routes)
  • No sensitive data exposed in logs, errors, or client responses (API token flows server-side only; status errors sanitized)
  • User input validated and sanitized at API boundaries (schema-layer validation, URLSearchParams encoding for query forwarding)
  • No new dependencies with known CVEs
  • No hardcoded credentials or secrets
  • CORS configuration unchanged
  • Error responses do not leak internal details (schema errors are by design; proxy errors are now sanitized)

All four round-1 findings are correctly and thoroughly remediated. The Security Audit wiki page has been updated to mark all four findings as Resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steilerDev steilerDev enabled auto-merge (squash) March 2, 2026 06:05
@steilerDev steilerDev merged commit 01fbd13 into beta Mar 2, 2026
9 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 2, 2026

🎉 This PR is included in version 1.10.0-beta.72 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 2, 2026

🎉 This PR is included in version 1.11.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@steilerDev steilerDev deleted the feat/354-paperless-proxy-service branch March 7, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant