Files
fuelboard/SECURITY.md
T

4.1 KiB
Raw Blame History

FuelBoard Security Audit

Audit date: 2026-08-11 · Scope: app, widget, relay, build pipeline, git history.

Summary

No critical issues. No secrets in source, git history, or binaries. One architecture note (relay IP baked into the app) is scheduled for removal by the govuk-API switch (BACKLOG P0). Findings below are ordered by severity.

Findings

1. LAN IP baked into app binary — MEDIUM (planned fix)

  • Shared/FuelPriceProvider.swift:52 — baseURL = URL(string: "http://192.168.1.131:8788")!
  • Confirmed present in the built app binary (strings).
  • Why it works: ATS exempts raw numeric IP addresses (documented Apple behaviour), so no NSAllowsArbitraryLoads exception exists in the plist — the plist is clean.
  • Risk: the relay is a private LAN host; the app is unusable off the user's network, and the URL is effectively a hardcoded endpoint.
  • Fix: the govuk API switch (BACKLOG P0) replaces this with the HTTPS gov.uk endpoint via the relay. For any future public build, the app should talk to an HTTPS domain, never a raw IP.

2. No secrets in source / git / binary — PASS

  • App + widget sources: no API keys, tokens, or passwords (only keychain API usage in FuelStore.swift and OAuth documentation comments in FuelPriceProvider.swift).
  • Relay: CLIENT_ID/CLIENT_SECRET read from environment only (app/main.py:31-32); .env is gitignored; __pycache__/*.pyc untracked; .env.example ships empty placeholders.
  • Binary scan: only the relay IP, maps.apple.com, and group.com.apt.fuelboard strings found. No credentials.

3. Price data hardening — PASS (three layers)

  • Relay rejects out-of-band rows at the edge (_fuel_price 50–500 pence/litre).
  • App drops out-of-band grades at decode (FuelPriceProvider.decodeStations, previously c1edd4d).
  • Station names title-cased at decode + on cache load (aee890e).
  • Covered by unit tests (FuelBoardTests — PriceGuardTests).

4. Keychain / app-group hygiene — PASS

  • Keychain access groups: com.apt.fuelboard.shared listed FIRST (default access group), then com.apt.fuelboard — matches the proven WidgetBoard pattern for free-account keychain sharing.
  • Stations stored in app-group UserDefaults only (~2.9 MB exceeds keychain limits; documented in FuelStore.swift). Stored data is public price data — not sensitive.
  • kSecAttrAccessibleAfterFirstUnlock — widget can read after reboot without unlock. Correct for background widget refresh.

5. Transport security — PASS for LAN dev, MUST CHANGE for public

  • No NSAllowsArbitraryLoads anywhere (confirmed by plist + pbxproj grep).
  • Cleartext HTTP only to the LAN relay IP (ATS-exempt). Once the relay moves behind an HTTPS domain (govuk switch), no ATS changes are needed.

6. Privacy / permissions — review for App Store

  • NSLocationWhenInUseUsageDescription + NSLocationAlwaysAndWhenInUseUsageDescription both present with clear copy. The app deliberately does not include the location value in UIBackgroundModes; optional alerts use CoreLocation region monitoring/significant-change behaviour rather than persistent real-time background tracking.
  • Location is the only sensitive permission requested. No contacts, photos, camera, mic.
  • No third-party SDKs → no SDK privacy manifests required.

What was checked

  • Source secret scan (app, widget, shared, relay, config)
  • Git history scan (all 43 commits, both repos)
  • Built binary string scan (app + widget)
  • Entitlements (app groups, keychain groups, ordering)
  • Info.plist (ATS, permissions, background modes)
  • Relay env handling + gitignore + pycache hygiene
  • Data validation layers (relay band, app decode guard, sanitizer)

Recurring checks

Run before every release:

# secrets in working tree
grep -rniE "api[_-]?key|client[_-]?secret|password|token" FuelBoard/ Shared/ FuelBoardWidgets/ --include="*.swift"

# secrets in git history (both repos)
git grep -ilE "client_secret|api_key|password" $(git rev-list --all) | head

# binary leak check (after build)
strings <app-binary> | grep -iE "192\.168|100\.120|client_secret|api_key"