polaris-maps
Decentralized P2P Maps + Navigation
rad:zHUKM29kn6E97oJ6FwTSYkcPxTiZ
Visibility
public
Delegates
did:key:z6MkkucZJnztmvGn2GawftUuXjCE57hQJkWAKB3h1g7D8LBY
Default branch
main → 8a25db5d1e19fe7228598dfa85ff3978ff4523b7 (Mon Apr 13 09:48:25 2026)
Threshold
1
README.md
# Polaris Maps

A decentralized, peer-to-peer mapping application built with React Native / Expo. Polaris Maps combines real-time traffic data, offline vector maps, point-of-interest contribution, and a DePIN (Decentralized Physical Infrastructure Network) incentive layer.

---

## Table of Contents

- [Features](#features)
- [Architecture](#architecture)
- [Tech Stack](#tech-stack)
- [Getting Started](#getting-started)
- [Environment Variables](#environment-variables)
- [Project Structure](#project-structure)
- [Testing](#testing)
- [πŸ” Security Review](#-security-review)

---

## Features

| Feature                                         | Status |
| ----------------------------------------------- | ------ |
| P2P map tile sharing via Hypercore / Hyperdrive | βœ…     |
| Real-time traffic overlay (TomTom + HERE)       | βœ…     |
| Dynamic ETA calculation with traffic adjustment | βœ…     |
| Offline region tile packs                       | βœ…     |
| Point-of-interest browsing, editing & review    | βœ…     |
| Street-level imagery capture & viewer           | βœ…     |
| Decentralized identity (secp256k1 keypair)      | βœ…     |
| Dark mode with custom Apple Maps–inspired style | βœ…     |
| Navigation mode with heading-up camera          | βœ…     |
| OSM-backed local geocoding + Nominatim fallback | βœ…     |

---

## Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Expo Router (app/)          React Native UI         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  (tabs)  β”‚  β”‚   poi/   β”‚  β”‚  regions/       β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  State Layer (Zustand stores)                        β”‚
β”‚  mapStore Β· trafficStore Β· osmPoiStore               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Service Layer (src/services/)                       β”‚
β”‚  traffic/ Β· routing/ Β· geocoding/ Β· poi/ Β· sync/    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Native / P2P Bridge                                 β”‚
β”‚  nodejs-assets/ ← Hyperswarm Β· Hyperdrive Β· Gun.js  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  External APIs                                       β”‚
β”‚  TomTom  HERE  Valhalla  Overpass  Nominatim         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## Tech Stack

| Layer      | Technology                                                 |
| ---------- | ---------------------------------------------------------- |
| Framework  | React Native 0.76.9 + Expo SDK 52 (bare workflow)          |
| Language   | TypeScript 5.6.3 (strict)                                  |
| Navigation | Expo Router 4 + React Navigation 7                         |
| Maps       | MapLibre React Native 10 (alpha) + OpenFreeMap tiles       |
| State      | Zustand 5                                                  |
| Storage    | expo-sqlite (FTS5) Β· react-native-mmkv Β· expo-secure-store |
| Identity   | @noble/curves secp256k1 Β· @noble/hashes SHA-256            |
| P2P        | Hyperswarm Β· Hyperdrive Β· Hypercore Β· GunDB                |
| Traffic    | TomTom Traffic Flow v4 Β· HERE Traffic Flow v7              |
| Routing    | Valhalla (online)                                          |
| Geocoding  | expo-sqlite FTS5 local Β· Nominatim fallback                |
| Testing    | Jest 29 Β· @testing-library/react-native 12                 |
| Linting    | ESLint 9 flat config Β· Prettier                            |
| Commits    | Commitlint + Husky                                         |

---

## Getting Started

### Prerequisites

- Node.js β‰₯ 20
- pnpm β‰₯ 9
- Xcode 16 (iOS) or Android Studio (Android)
- CocoaPods (iOS: `sudo gem install cocoapods`)

### Install

```bash
git clone https://github.com/your-org/polaris-maps.git
cd polaris-maps
pnpm install
# iOS only
cd ios && pod install && cd ..
```

### Run

```bash
# iOS Simulator
pnpm ios

# Android Emulator
pnpm android

# Metro bundler only
pnpm start
```

---

## Environment Variables

Create a `.env` file in the project root (never commit it):

```env
EXPO_PUBLIC_TOMTOM_API_KEY=your_tomtom_key
EXPO_PUBLIC_HERE_API_KEY=your_here_key
EXPO_PUBLIC_TOMTOM_PROXY_URL=https://your-proxy-server/tomtom   # optional β€” preferred over direct key
```

---

## Project Structure

```
src/
  components/map/       MapView, TrafficOverlay, TrafficRouteLayer, POILayer
  constants/            config.ts, theme.ts, darkMapStyle.ts
  contexts/             ThemeContext
  hooks/                useLocation, useNavigation, useTraffic
  models/               traffic.ts, poi.ts, user.ts
  services/
    geocoding/          FTS5 local search + Nominatim
    identity/           keypair generation (SecureStore)
    poi/                OSM Overpass fetcher
    routing/            Valhalla routing
    storage/            MMKV singleton, SQLite helpers
    traffic/            tomtomFetcher, hereFetcher, trafficMerger, routeTrafficService
  stores/               mapStore, trafficStore, osmPoiStore
  types/                modules.d.ts (ambient declarations)
  utils/                etaCalculator, polyline, geohash
app/
  (tabs)/               index, navigation, search, profile
  imagery/              capture, viewer
  onboarding/
  poi/                  [id], edit, reviews
  regions/              index, offline
  settings/
nodejs-assets/nodejs-project/   Hyperswarm / Hyperdrive P2P bridge (Node.js side-channel)
specs/                  Feature specs, plans & research docs
```

---

## Testing

```bash
pnpm test            # jest with --passWithNoTests
pnpm typecheck       # tsc --noEmit
pnpm lint            # eslint
```

---

## πŸ” Security Review

**Date:** 2026-04-13
**Standard:** OWASP Top 10:2025
**Scope:** All source files under `src/`, `app/`, `nodejs-assets/`, `package.json`, `index.js` (34 files)
**Reviewer:** GitHub Copilot automated audit
**Tests:** 50 regression tests in `__tests__/unit/security.test.ts`

> This section is kept continuously up-to-date. Every change to the codebase triggers a re-evaluation of all applicable findings.

### Summary

| ID                  | Severity  | Status      | OWASP     | Location                                                | Title                                                             |
| ------------------- | --------- | ----------- | --------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| [F-001](#f-001)     | πŸ”΄ High   | βœ… Fixed    | A08 / A05 | `nodejs-assets/nodejs-project/index.js:310`             | Path traversal in Hyperdrive download β€” no escape guard           |
| [F-002](#f-002)     | πŸ”΄ High   | ⚠️ Accepted | A04       | `src/constants/config.ts:32,35`                         | TomTom + HERE API keys bundled into client via `EXPO_PUBLIC_`     |
| [F-003](#f-003)     | 🟑 Medium | βœ… Fixed    | A05       | `app/poi/[id].tsx:110,119`                              | Unvalidated external URL opened via `Linking.openURL`             |
| [F-004](#f-004)     | 🟑 Medium | βœ… Fixed    | A05       | `src/services/geocoding/geocodingService.ts:23`         | FTS5 query injection via `"` in search input β€” crashes search     |
| [F-005](#f-005)     | 🟑 Medium | βœ… Fixed    | A03       | `package.json`, `nodejs-assets/package.json`            | All deps use `^`/`~` ranges β€” unpinned supply chain               |
| [F-006](#f-006)     | 🟑 Medium | βœ… Fixed    | A06       | `src/services/overpassClient.ts`, `geocodingService.ts` | Insufficient rate limiting on Overpass / Nominatim                |
| [F-007](#f-007)     | πŸ”΅ Low    | βœ… Fixed    | A04       | `src/services/storage/mmkv.ts:3`                        | MMKV storage created without encryption                           |
| [F-008](#f-008)     | πŸ”΅ Low    | βœ… Fixed    | A09       | `src/services/traffic/trafficFlowService.ts:43`         | Full `Error` object logged β€” may leak API key in stack trace      |
| [F-009](#f-009)     | πŸ”΅ Low    | βœ… Fixed    | A10       | `src/services/traffic/hereFetcher.ts:84`                | `fetchHERETraffic` lacks `try/catch` β€” loading spinner gets stuck |
| [F-010](#f-010)     | πŸ”΅ Low    | βœ… Fixed    | A10       | `src/services/geocoding/geocodingService.ts:18`         | FTS5 exception propagates unhandled to UI                         |
| [F-011](#f-011)     | πŸ”΅ Low    | βœ… Fixed    | A09       | `src/services/routing/routingService.ts:100`            | Raw Valhalla error response body included in thrown `Error`       |
| [F-012](#f-012)     | βœ… Clear  | β€”           | A01       | β€”                                                       | No server endpoints β€” access control not applicable               |
| [F-013](#f-013)     | βœ… Clear  | β€”           | A07       | `src/services/identity/keypair.ts`                      | Private key in SecureStore; strong curve; no weak auth flows      |
| [F-014](#f-014)     | N/A       | β€”           | A01       | `app/poi/[id].tsx`                                      | IDOR not applicable β€” all POI reads are local SQLite              |
| [NEW-001](#new-001) | πŸ”΄ High   | βœ… Fixed    | A08       | `src/services/traffic/nostrFallback.ts:242`             | Nostr event signatures not verified β€” fake traffic injection      |
| [NEW-002](#new-002) | 🟑 Medium | ⚠️ Noted    | A04       | `src/services/gun/init.ts`, `offlineQueue.ts`           | Deprecated unencrypted MMKV still used for P2P + offline queue    |
| [NEW-003](#new-003) | 🟑 Medium | βœ… Fixed    | A05       | `src/services/poi/osmFetcher.ts:211`                    | Overpass QL injection via unescaped `"` in `fetchOsmPoisByName`   |
| [NEW-004](#new-004) | 🟑 Medium | βœ… Fixed    | A01       | `nodejs-assets/nodejs-project/index.js:39`              | No path validation on `gunzip` command handler                    |
| [NEW-005](#new-005) | 🟒 Low    | βœ… Fixed    | A04       | `src/services/identity/signing.ts:17`                   | `createSigningPayload` lacks domain separation                    |
| [NEW-006](#new-006) | 🟒 Low    | βœ… Fixed    | A05       | `src/services/poi/osmFetcher.ts:144`                    | Overpass QL tag interpolation in `fetchOsmPoisByTags`             |

---

### Detailed Findings

#### F-001

**Severity:** πŸ”΄ High β†’ βœ… Fixed (2026-04-13)
**OWASP:** A08 – Integrity Failures / A05 – Injection
**Title:** Path Traversal in Hyperdrive Download β€” No Escape Guard
**File:** `nodejs-assets/nodejs-project/index.js` lines 310–328

**What was wrong:**
`handleHdDownload()` iterated over every entry in a Hyperdrive served by an untrusted P2P peer and wrote each file using `path.join(destDir, entry.key)` with no path-traversal check. An adversarial peer could write files outside the intended directory.

**Fix applied:**
Added `path.resolve()` + `startsWith(resolvedDest + path.sep)` guard, matching the existing tar-extraction handler. Entries that resolve outside `destDir` are silently skipped.

**Regression test:** `__tests__/unit/security.test.ts` β€” "Hyperdrive download path traversal guard (F-001)"

---

#### F-002

**Severity:** πŸ”΄ High β€” ⚠️ Accepted Risk
**OWASP:** A04 – Cryptographic Failures
**Title:** TomTom + HERE API Keys Bundled into Client Bundle via `EXPO_PUBLIC_`
**File:** `src/constants/config.ts` lines 32, 35

**Description:**
`tomtomApiKey` and `hereApiKey` are sourced via `process.env.EXPO_PUBLIC_TOMTOM_API_KEY` and `process.env.EXPO_PUBLIC_HERE_API_KEY`. Expo's `EXPO_PUBLIC_` convention statically inlines environment variables into the JavaScript bundle at build time.

**Mitigation:**

- Restrict TomTom/HERE keys in their dashboards to the app's bundle ID and platform signature.
- API key redaction has been added to all error logging paths (F-008, F-011) to prevent accidental leak via crash reporters.
- A backend proxy is the recommended long-term fix but is not currently in scope.

---

#### F-003

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A05 – Injection
**Title:** Unvalidated External URL Opened via `Linking.openURL` in POI Detail Screen
**File:** `app/poi/[id].tsx`

**What was wrong:**
`Linking.openURL` was called on raw POI data (phone, website, social URLs) without URL scheme validation. Malicious P2P/OSM data could inject `intent://` or `javascript:` schemes.

**Fix applied:**
Added `safeOpenURL()` helper that validates URL scheme against `['https:', 'http:']` allowlist, and `safePhone()` that strips all characters except `0-9+#*`. All `Linking.openURL` calls in the file now go through these helpers.

**Regression test:** `__tests__/unit/security.test.ts` β€” "URL scheme validation (F-003)"

---

#### F-004

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A05 – Injection
**Title:** FTS5 Query Injection via Unescaped `"` in Search Input β€” Crashes Search
**File:** `src/services/geocoding/geocodingService.ts`

**What was wrong:**
FTS5 query builder wrapped tokens in double-quotes without escaping, so a literal `"` in user input created malformed FTS5 syntax, crashing SQLite.

**Fix applied:**
Double-quotes are now stripped from each token before wrapping: `w.replace(/"/g, '')`. Additionally, `searchAddressLocal` is wrapped with `.catch(() => [])` so any remaining SQLite errors gracefully fall through to Nominatim.

**Regression test:** `__tests__/unit/security.test.ts` β€” "FTS5 query injection prevention (F-004)"

---

#### F-005

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A03 – Supply Chain Failures
**Title:** All Dependencies Use Unpinned `^`/`~` Version Ranges
**File:** `package.json`, `nodejs-assets/nodejs-project/package.json`

**Fix applied:**

- All 42 production dependencies in `package.json` pinned to exact versions.
- All 4 dependencies in `nodejs-assets/nodejs-project/package.json` pinned to exact versions.
- Committed `pnpm-lock.yaml` for `nodejs-assets/nodejs-project/`.
- devDependencies retain `^`/`~` ranges, mitigated by lockfile and `--frozen-lockfile` in CI.

---

#### F-006

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A06 – Insecure Design
**Title:** Insufficient Rate Limiting on Overpass API and Nominatim
**File:** `src/services/overpassClient.ts`, `src/services/geocoding/geocodingService.ts`

**Fix applied:**

- Added 1 000 ms minimum inter-request gap to `overpassFetch()` in `overpassClient.ts`.
- Added 1 000 ms minimum inter-request gap to `searchAddressNominatim()` in `geocodingService.ts`.
- Both enforcers use timestamp tracking with `await` backoff, independent of UI debounce timers.

---

#### F-007

**Severity:** πŸ”΅ Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A04 – Cryptographic Failures
**Title:** MMKV Storage Instance Created Without Encryption
**File:** `src/services/storage/mmkv.ts`

**Fix applied:**
Added `getStorage()` async initializer that generates a random encryption key on first launch (via `expo-crypto`) and stores it in `expo-secure-store` (iOS Keychain / Android Keystore). MMKV is now created with `encryptionKey`. The synchronous `storage` export is retained as deprecated for backward compatibility during migration.

---

#### F-008

**Severity:** πŸ”΅ Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A09 – Logging Failures
**Title:** Full `Error` Object Logged β€” May Expose API Key in Stack Trace
**File:** `src/services/traffic/trafficFlowService.ts`

**Fix applied:**
Error messages are now redacted with `error.message.replace(/key=[^&]*/g, 'key=REDACTED')` before logging. The raw `Error` object is no longer passed to `console.warn`.

**Regression test:** `__tests__/unit/security.test.ts` β€” "API key redaction in error logs (F-008, F-011)"

---

#### F-009

**Severity:** πŸ”΅ Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A10 – Exceptional Conditions
**Title:** `fetchHERETraffic` Lacks `try/catch` β€” Loading State Gets Stuck
**File:** `src/services/traffic/hereFetcher.ts`

**Fix applied:**
Wrapped the entire fetch + JSON parse block in `try/catch`, returning `[]` on any exception.

---

#### F-010

**Severity:** πŸ”΅ Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A10 – Exceptional Conditions
**Title:** FTS5 Exception Propagates Unhandled to UI
**File:** `src/services/geocoding/geocodingService.ts`

**Fix applied:**
`searchAddressLocal()` call is now wrapped with `.catch(() => [])`, gracefully falling through to Nominatim on any SQLite error. See F-004 fix.

---

#### F-011

**Severity:** πŸ”΅ Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A09 – Logging Failures
**Title:** Raw Valhalla Error Response Body Included in Thrown `Error`
**File:** `src/services/routing/routingService.ts`

**Fix applied:**
Response body is now truncated to 200 characters and redacted with `replace(/key=[^&]*/g, 'key=REDACTED')` before being included in the thrown error message.

**Regression test:** `__tests__/unit/security.test.ts` β€” "API key redaction in error logs (F-008, F-011)"

---

#### F-012

**Severity:** βœ… Clear
**OWASP:** A01 – Broken Access Control

No server-side endpoints are defined in this codebase β€” it is a fully client-side mobile application. All data access is either local (SQLite, MMKV) or directed at third-party public APIs (TomTom, HERE, Valhalla, Nominatim, Overpass). The P2P Hyperdrive data sharing uses cryptographic Hypercore keys as access tokens. Access control at the server level is not applicable.

---

#### F-013

**Severity:** βœ… Clear
**OWASP:** A07 – Authentication Failures
**File:** `src/services/identity/keypair.ts`

User identity is based on a `secp256k1` keypair generated via `@noble/curves` (a well-audited, constant-time implementation). The private key is generated with `schnorr.utils.randomPrivateKey()` (CSPRNG-backed) and stored **exclusively** in `expo-secure-store`, which maps to the iOS Keychain and Android Keystore. It is never written to MMKV, AsyncStorage, or any less-secure storage. No password-based authentication is implemented. This is appropriate for a decentralized app with no central auth server.

---

#### F-014

**Severity:** N/A
**OWASP:** A01 – Broken Access Control (IDOR)
**File:** `app/poi/[id].tsx`

`app/poi/[id].tsx` reads a POI by ID from the local POI store (`getPlaceById`). The `id` path parameter comes from Expo Router's `useLocalSearchParams`. Since this is a local SQLite read on-device with no remote endpoint, there is no IDOR surface β€” the user can only read their own device's data.

---

#### NEW-001

**Severity:** πŸ”΄ High β†’ βœ… Fixed (2026-04-13)
**OWASP:** A08 – Software and Data Integrity Failures
**Title:** Nostr Event Signatures Not Verified β€” Fake Traffic Data Injection
**File:** `src/services/traffic/nostrFallback.ts` β€” `processIncomingEvent()`

**What was wrong:**
Incoming Nostr events were parsed and dispatched to traffic probe callbacks without verifying the NIP-01 Schnorr signature (`event.sig` over `event.id`), nor verifying that `event.id` matches `sha256([0, pubkey, created_at, kind, tags, content])`. A malicious relay (or MITM) could forge events with arbitrary pubkeys and inject fake traffic congestion data, causing incorrect ETAs and route recommendations.

**Fix applied:**
Added `verifyEvent()` function that: (1) recomputes `event.id` from `sha256(JSON.stringify([0, pubkey, created_at, kind, tags, content]))` and compares against the claimed ID, (2) verifies the Schnorr signature via `schnorr.verify()`. Events failing either check are silently dropped. The `schnorr` and `sha256` imports were already present in the file.

**Regression test:** `__tests__/unit/security.test.ts` β€” "Nostr event signature verification (NEW-001)"

---

#### NEW-002

**Severity:** 🟑 Medium β€” ⚠️ Noted (migration required)
**OWASP:** A04 – Cryptographic Failures
**Title:** Deprecated Unencrypted MMKV Export Still Used for P2P Data and Offline Queue
**File:** `src/services/gun/init.ts`, `src/services/sync/offlineQueue.ts`

**Description:**
Both files import the deprecated **unencrypted** `storage` export from `mmkv.ts`. Gun.js relay cache, all P2P data (place edits, reviews, attestations), and queued traffic probes are stored in plaintext on device. On a rooted/jailbroken device, this data is trivially extractable.

There are 12 total callers of the deprecated `storage` export across the codebase. Migrating to `getStorageSync()` requires ensuring `getStorage()` is called at app startup before any synchronous access β€” a cross-cutting change that needs careful sequencing.

**Recommended fix:** Add `getStorage()` call in the root `_layout.tsx` before any service initialization, then migrate all 12 callers from `storage` to `getStorageSync()`.

---

#### NEW-003

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A05 – Injection
**Title:** Overpass QL Injection via Unescaped `"` in `fetchOsmPoisByName`
**File:** `src/services/poi/osmFetcher.ts` β€” `fetchOsmPoisByName()`

**What was wrong:**
The regex escape function escaped regex metacharacters but **not** double-quote (`"`). User search input containing `"` would break out of the Overpass QL string literal: `"name"~"user"input"`, causing syntax errors or altered query semantics against public Overpass instances.

**Fix applied:**
Added `"` to the escape regex character class: `.replace(/[.*+?^${}()|[\]\\"]/g, '\\$&')`.

**Regression test:** `__tests__/unit/security.test.ts` β€” "Overpass QL double-quote injection prevention (NEW-003)"

---

#### NEW-004

**Severity:** 🟑 Medium β†’ βœ… Fixed (2026-04-13)
**OWASP:** A01 – Broken Access Control
**Title:** No Path Validation on `gunzip` Command Handler
**File:** `nodejs-assets/nodejs-project/index.js` β€” `case 'gunzip'`

**What was wrong:**
The `gunzip` handler accepted `inputPath` and `outputPath` from the React Native bridge with no validation that paths stay within expected directories. While the bridge is internal, a compromised or buggy RN module could read from or write to arbitrary filesystem locations. Both `extract-tar` and `hd-download` already had path traversal guards.

**Fix applied:**
Added `path.resolve(outputPath)` + `startsWith(homeDir + path.sep)` guard, consistent with the other handlers. Requests with `outputPath` outside the app's home directory are rejected with an error response.

**Regression test:** `__tests__/unit/security.test.ts` β€” "Gunzip output path validation (NEW-004)"

---

#### NEW-005

**Severity:** 🟒 Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A04 – Cryptographic Failures
**Title:** `createSigningPayload` Lacks Domain Separation
**File:** `src/services/identity/signing.ts`

**What was wrong:**
`createSigningPayload(...fields)` joined fields with an empty string: `fields.map(String).join('')`. This meant `("ab","c")` and `("a","bc")` would produce the identical payload `"abc"`, creating an ambiguity that could permit cross-context signature reuse if an attacker controls adjacent field values.

**Fix applied:**
Changed the join delimiter to null byte (`\0`): `fields.map(String).join('\0')`. Null bytes cannot appear in any of the current field values (UUIDs, timestamps, field names).

**Regression test:** `__tests__/unit/security.test.ts` β€” "Signing payload domain separation (NEW-005)"

---

#### NEW-006

**Severity:** 🟒 Low β†’ βœ… Fixed (2026-04-13)
**OWASP:** A05 – Injection
**Title:** Overpass QL Tag Interpolation in `fetchOsmPoisByTags`
**File:** `src/services/poi/osmFetcher.ts` β€” `fetchOsmPoisByTags()`

**What was wrong:**
`extraFilters` key/value pairs were interpolated directly into Overpass QL queries: `` `["${k}"="${v}"]` ``. Currently safe because inputs originate from internal category mappings, but a future code path providing user-controlled values would introduce injection.

**Fix applied:**
Added `.replace(/"/g, '')` to both `k` and `v` in the `extraFilters` map, stripping any double-quotes as defense-in-depth.

---

### Priority Action Plan

| Priority                | Finding                                                 | Status       |
| ----------------------- | ------------------------------------------------------- | ------------ |
| πŸ”΄ P1 β€” Fix immediately | F-001: Path traversal in Hyperdrive download            | βœ… Fixed     |
| πŸ”΄ P1 β€” Fix immediately | F-003: Unvalidated `Linking.openURL` in `[id].tsx`      | βœ… Fixed     |
| πŸ”΄ P1 β€” Fix immediately | NEW-001: Nostr event signatures not verified            | βœ… Fixed     |
| πŸ”΄ P2 β€” Fix this sprint | F-004: FTS5 `"` injection crashes search                | βœ… Fixed     |
| πŸ”΄ P2 β€” Fix this sprint | F-002: API keys in client bundle                        | ⚠️ Accepted  |
| 🟑 P3 β€” Fix next sprint | F-005: Unpinned dependencies                            | βœ… Fixed     |
| 🟑 P3 β€” Fix next sprint | F-007: Unencrypted MMKV                                 | βœ… Fixed     |
| 🟑 P3 β€” Fix next sprint | NEW-002: Deprecated unencrypted MMKV callers (12 files) | ⚠️ Migration |
| 🟑 P3 β€” Fix next sprint | NEW-003: Overpass QL `"` injection                      | βœ… Fixed     |
| 🟑 P3 β€” Fix next sprint | NEW-004: Gunzip path validation                         | βœ… Fixed     |
| 🟒 P4 β€” Ongoing hygiene | F-006, F-008, F-009, F-010, F-011, NEW-005, NEW-006     | βœ… All Fixed |

---

_Security review last updated: 2026-04-13. Re-run audit after each significant code change._