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:
@@ -16,7 +16,7 @@ const mockStation: Station = {
|
||||
longitude: -122.4194,
|
||||
rating: 4.2,
|
||||
distance: 250,
|
||||
photoUrl: 'https://example.com/photo.jpg'
|
||||
photoReference: 'mock-photo-reference'
|
||||
};
|
||||
|
||||
describe('StationCard', () => {
|
||||
@@ -38,7 +38,7 @@ describe('StationCard', () => {
|
||||
|
||||
const photo = screen.getByAltText('Shell Gas Station');
|
||||
expect(photo).toBeInTheDocument();
|
||||
expect(photo).toHaveAttribute('src', 'https://example.com/photo.jpg');
|
||||
expect(photo).toHaveAttribute('src', '/api/stations/photo/mock-photo-reference');
|
||||
});
|
||||
|
||||
it('should render rating when available', () => {
|
||||
@@ -54,7 +54,7 @@ describe('StationCard', () => {
|
||||
});
|
||||
|
||||
it('should not crash when photo is missing', () => {
|
||||
const stationWithoutPhoto = { ...mockStation, photoUrl: undefined };
|
||||
const stationWithoutPhoto = { ...mockStation, photoReference: undefined };
|
||||
render(<StationCard station={stationWithoutPhoto} isSaved={false} />);
|
||||
|
||||
expect(screen.getByText('Shell Gas Station')).toBeInTheDocument();
|
||||
|
||||
@@ -18,6 +18,7 @@ import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import DirectionsIcon from '@mui/icons-material/Directions';
|
||||
import { Station, SavedStation } from '../types/stations.types';
|
||||
import { formatDistance } from '../utils/distance';
|
||||
import { getStationPhotoUrl } from '../utils/photo-utils';
|
||||
|
||||
interface StationCardProps {
|
||||
station: Station;
|
||||
@@ -83,11 +84,11 @@ export const StationCard: React.FC<StationCardProps> = ({
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{station.photoUrl && (
|
||||
{station.photoReference && (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="200"
|
||||
image={station.photoUrl}
|
||||
image={getStationPhotoUrl(station.photoReference)}
|
||||
alt={station.name}
|
||||
sx={{ objectFit: 'cover' }}
|
||||
/>
|
||||
|
||||
@@ -56,8 +56,8 @@ export interface Station {
|
||||
rating: number;
|
||||
/** Distance from search location in meters */
|
||||
distance?: number;
|
||||
/** URL to station photo if available */
|
||||
photoUrl?: string;
|
||||
/** Photo reference for station photo if available */
|
||||
photoReference?: string;
|
||||
/** Whether the station is saved for the user */
|
||||
isSaved?: boolean;
|
||||
/** Saved-station metadata if applicable */
|
||||
|
||||
15
frontend/src/features/stations/utils/photo-utils.ts
Normal file
15
frontend/src/features/stations/utils/photo-utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @ai-summary Helper utilities for station photos
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate secure photo URL using backend proxy endpoint
|
||||
* This prevents exposing the Google Maps API key to the frontend
|
||||
*/
|
||||
export function getStationPhotoUrl(photoReference: string | undefined): string | undefined {
|
||||
if (!photoReference) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `/api/stations/photo/${encodeURIComponent(photoReference)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user