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:
@@ -65,7 +65,6 @@ export class GoogleMapsClient {
|
||||
}
|
||||
|
||||
private transformPlaceToStation(place: GooglePlace, searchLat: number, searchLng: number): Station {
|
||||
// Calculate distance from search point
|
||||
const distance = this.calculateDistance(
|
||||
searchLat,
|
||||
searchLng,
|
||||
@@ -73,10 +72,10 @@ export class GoogleMapsClient {
|
||||
place.geometry.location.lng
|
||||
);
|
||||
|
||||
// Generate photo URL if available
|
||||
let photoUrl: string | undefined;
|
||||
// Store photo reference instead of full URL to avoid exposing API key
|
||||
let photoReference: string | undefined;
|
||||
if (place.photos && place.photos.length > 0 && place.photos[0]) {
|
||||
photoUrl = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photo_reference=${place.photos[0].photo_reference}&key=${this.apiKey}`;
|
||||
photoReference = place.photos[0].photo_reference;
|
||||
}
|
||||
|
||||
const station: Station = {
|
||||
@@ -89,9 +88,8 @@ export class GoogleMapsClient {
|
||||
distance
|
||||
};
|
||||
|
||||
// Only set optional properties if defined
|
||||
if (photoUrl !== undefined) {
|
||||
station.photoUrl = photoUrl;
|
||||
if (photoReference !== undefined) {
|
||||
station.photoReference = photoReference;
|
||||
}
|
||||
|
||||
if (place.opening_hours?.open_now !== undefined) {
|
||||
@@ -105,6 +103,31 @@ export class GoogleMapsClient {
|
||||
return station;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch photo from Google Maps API using photo reference
|
||||
* Used by photo proxy endpoint to serve photos without exposing API key
|
||||
*/
|
||||
async fetchPhoto(photoReference: string, maxWidth: number = 400): Promise<Buffer> {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.baseURL}/photo`,
|
||||
{
|
||||
params: {
|
||||
photo_reference: photoReference,
|
||||
maxwidth: maxWidth,
|
||||
key: this.apiKey
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
);
|
||||
|
||||
return Buffer.from(response.data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch photo from Google Maps', { error, photoReference });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const R = 6371e3; // Earth's radius in meters
|
||||
const φ1 = lat1 * Math.PI / 180;
|
||||
|
||||
Reference in New Issue
Block a user