Security fix: Implement Google Maps API photo proxy (Fix 3)
Completed HIGH severity security fix (CVSS 6.5) to prevent Google Maps API key exposure to frontend clients. Issue: API key was embedded in photo URLs sent to frontend, allowing potential abuse and quota exhaustion. Solution: Implemented backend proxy endpoint for photos. Backend Changes: - google-maps.client.ts: Changed photoUrl to photoReference, added fetchPhoto() - stations.types.ts: Updated type definition (photoUrl → photoReference) - stations.controller.ts: Added getStationPhoto() proxy method - stations.routes.ts: Added GET /api/stations/photo/:reference route - stations.service.ts: Updated to use photoReference - stations.repository.ts: Updated database queries and mappings - admin controllers/services: Updated for consistency - Created migration 003 to rename photo_url column Frontend Changes: - stations.types.ts: Updated type definition (photoUrl → photoReference) - photo-utils.ts: NEW - Helper to generate proxy URLs - StationCard.tsx: Use photoReference with helper function Tests & Docs: - Updated mock data to use photoReference - Updated test expectations for proxy URLs - Updated API.md and TESTING.md documentation Database Migration: - 003_rename_photo_url_to_photo_reference.sql: Renames column in station_cache Security Benefits: - API key never sent to frontend - All photo requests proxied through authenticated endpoint - Photos cached for 24 hours (Cache-Control header) - No client-side API key exposure Files modified: 16 files New files: 2 (photo-utils.ts, migration 003) Status: All 3 P0 security fixes now complete - Fix 1: crypto.randomBytes() ✓ - Fix 2: Magic byte validation ✓ - Fix 3: API key proxy ✓ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ interface CreateStationBody {
|
||||
pricePremium?: number;
|
||||
priceDiesel?: number;
|
||||
rating?: number;
|
||||
photoUrl?: string;
|
||||
photoReference?: string;
|
||||
}
|
||||
|
||||
interface UpdateStationBody {
|
||||
@@ -35,7 +35,7 @@ interface UpdateStationBody {
|
||||
pricePremium?: number;
|
||||
priceDiesel?: number;
|
||||
rating?: number;
|
||||
photoUrl?: string;
|
||||
photoReference?: string;
|
||||
}
|
||||
|
||||
interface StationParams {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface CreateStationData {
|
||||
pricePremium?: number;
|
||||
priceDiesel?: number;
|
||||
rating?: number;
|
||||
photoUrl?: string;
|
||||
photoReference?: string;
|
||||
}
|
||||
|
||||
interface UpdateStationData {
|
||||
@@ -32,7 +32,7 @@ interface UpdateStationData {
|
||||
pricePremium?: number;
|
||||
priceDiesel?: number;
|
||||
rating?: number;
|
||||
photoUrl?: string;
|
||||
photoReference?: string;
|
||||
}
|
||||
|
||||
interface StationListResult {
|
||||
@@ -63,7 +63,7 @@ export class StationOversightService {
|
||||
let dataQuery = `
|
||||
SELECT
|
||||
id, place_id, name, address, latitude, longitude,
|
||||
price_regular, price_premium, price_diesel, rating, photo_url, cached_at
|
||||
price_regular, price_premium, price_diesel, rating, photo_reference, cached_at
|
||||
FROM station_cache
|
||||
`;
|
||||
const params: any[] = [];
|
||||
@@ -114,7 +114,7 @@ export class StationOversightService {
|
||||
pricePremium: data.pricePremium,
|
||||
priceDiesel: data.priceDiesel,
|
||||
rating: data.rating,
|
||||
photoUrl: data.photoUrl,
|
||||
photoReference: data.photoReference,
|
||||
};
|
||||
|
||||
await this.stationsRepository.cacheStation(station);
|
||||
@@ -198,9 +198,9 @@ export class StationOversightService {
|
||||
updates.push(`rating = $${paramIndex++}`);
|
||||
values.push(data.rating);
|
||||
}
|
||||
if (data.photoUrl !== undefined) {
|
||||
updates.push(`photo_url = $${paramIndex++}`);
|
||||
values.push(data.photoUrl);
|
||||
if (data.photoReference !== undefined) {
|
||||
updates.push(`photo_reference = $${paramIndex++}`);
|
||||
values.push(data.photoReference);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
@@ -429,7 +429,7 @@ export class StationOversightService {
|
||||
pricePremium: row.price_premium ? parseFloat(row.price_premium) : undefined,
|
||||
priceDiesel: row.price_diesel ? parseFloat(row.price_diesel) : undefined,
|
||||
rating: row.rating ? parseFloat(row.rating) : undefined,
|
||||
photoUrl: row.photo_url,
|
||||
photoReference: row.photo_reference,
|
||||
lastUpdated: row.cached_at,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user