Skip to content

bug: /api/proxy disables TLS verification process-wide, permanently #139

Description

@jherforth

Summary

GET /api/proxy disables TLS certificate verification for the entire Node process, permanently, the first time it proxies any https:// URL. It is never restored, so every later outbound HTTPS call the backend makes — Google OAuth token exchange, Google Photos, Apple CalDAV, calendar feeds, and now Home Assistant — runs unverified for the remaining life of the container.

server/index.js:4336:

// For HTTP requests, ensure we don't have HTTPS-specific configurations
if (target.protocol === 'http:') {
  console.log('Making HTTP request (not HTTPS)');
  // No special HTTPS agent needed for HTTP
} else {
  console.log('Making HTTPS request');
  // For HTTPS, we might need to handle self-signed certificates
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Only for development
}

The trailing comment says "Only for development", but there is no guard on NODE_ENV or DEMO_MODE — this runs in production.

Why it is worse than it looks

NODE_TLS_REJECT_UNAUTHORIZED is a process-global that Node reads at connection time, not at startup. So this is not scoped to the proxied request:

  1. It applies to connections to other hosts, not just the proxy target.
  2. It applies to connections made later, including ones already in flight elsewhere.
  3. Nothing ever sets it back to 1.

One proxied HTTPS request permanently downgrades the whole backend.

Reproduction

Standalone script — no HomeGlow needed, just the same line the route executes. It targets self-signed.badssl.com, which exists to serve an invalid certificate:

const TARGET = 'https://self-signed.badssl.com/';

const attempt = async (label) => {
  try {
    const res = await fetch(TARGET);
    console.log(`${label} -> CONNECTED (HTTP ${res.status}) — certificate NOT verified`);
  } catch (err) {
    console.log(`${label} -> rejected (${err.cause?.code}) — certificate verified`);
  }
};

await attempt('BEFORE any /api/proxy request');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';   // exactly what index.js:4336 does
await attempt('AFTER, on an unrelated later connection');

Output on Node 24:

NODE_TLS_REJECT_UNAUTHORIZED at process start: undefined

  BEFORE any /api/proxy request
    -> rejected (DEPTH_ZERO_SELF_SIGNED_CERT) — certificate verified

  [simulating one HTTPS request through GET /api/proxy]

(node:26104) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0'
makes TLS connections and HTTPS requests insecure by disabling certificate verification.

  AFTER, on an unrelated later connection
    -> CONNECTED (HTTP 200) — certificate NOT verified

NODE_TLS_REJECT_UNAUTHORIZED now: "0" (never restored)

Node emits its own warning about this, which is a reasonable signal that it is not a supported pattern.

Blast radius

Every outbound HTTPS caller in the backend becomes unverified once the flag flips:

Caller What flows over it
services/googleConnection.js OAuth token exchange and refresh — client secret, access + refresh tokens
services/googlePhotos.js, googlePhotosPicker.js, googleCalendar.js Bearer-token API calls
services/appleCalDAV.js Apple ID + app-specific password
services/calendarSync.js Subscribed ICS feeds
services/weather/openweathermap.js API key in the query string
services/homeAssistant.js Long-lived access token (whole-home control)

The Google refresh-token exchange is the one that concerns me most: those are long-lived credentials, and a machine-in-the-middle on the LAN could present any certificate once the flag is set.

Mitigating context, so this is not overstated: HomeGlow is LAN-only by design, /api/proxy is hostname-whitelisted (empty by default apart from calapi.inadiutorium.cz), and it is blocked entirely in demo mode. Triggering the flip requires someone to have added an https:// host to PROXY_WHITELIST and hit the route. So this is a latent downgrade rather than an actively exploited hole — but the fix is small and the failure mode is silent.

Why this is worth fixing now rather than later

Home Assistant behind a self-signed certificate is an extremely common self-hosted setup, and it is precisely the case that would tempt someone to reach for this flag again. Fixing it before the Home Assistant integration grows (control panel, entity alerts — #57) means the right pattern is already in place.

Suggested fix

Never touch the global. Scope the relaxation to the one request with an explicit agent:

const https = require('node:https');

// Opt-in per host, not a process-wide switch.
const allowSelfSigned = isSelfSignedAllowed(targetHostname);
const axiosConfig = {
  timeout: 15000,
  maxRedirects: 5,
  headers: { /* ... */ },
  validateStatus: (status) => status < 500,
  ...(allowSelfSigned
    ? { httpsAgent: new https.Agent({ rejectUnauthorized: false }) }
    : {}),
};

Design questions worth deciding rather than assuming:

  • Should self-signed be allowed at all? The simplest correct fix is to delete the line and let self-signed targets fail with a clear error. That is a behaviour change for anyone currently relying on it, though I doubt anyone is, given the whitelist starts effectively empty.
  • If it stays, make it explicit and per-host — e.g. an allow_insecure_tls list in settings, surfaced in the Admin Panel with a visible warning, rather than silently applying to everything.

I'd lean toward deleting it and adding the per-host opt-in only when a real need appears.

Notes

  • Found while implementing feat: Home Assistant Integration #57 (Home Assistant weather); deliberately not fixed in that branch to keep the change reviewable and because the "should self-signed be supported" question deserves its own decision.
  • Related maintenance backlog: maintenance: npm packages #120.
  • The same route has two smaller shortcomings worth folding in if this is touched: it is GET-only and forwards no request headers, which is why the Home Assistant integration needed its own authenticated path rather than reusing the proxy.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

Status
Ready to Review

Relationships

None yet

Development

No branches or pull requests

Issue actions