getting google maps to leak data (exact pano dates)

written on 2024-08-18, last updated 2024-08-18

if you go on google maps a lot (and if you are reading this blog, you probably do), you have probably already noticed that while google maps shares the month a streetview panorama was taken, there is no way to find out the exact date.

a screenshot of google maps, with the date shown as Jul 2024 without further details
see - it's just a month with no date!

this makes some degree of sense: when google maps first launched, a lot of people had (somewhat understandable) privacy concerns (hello blurmany), and theoretically, knowing the exact date of the pano could be used for nefarious purposes. however, the risk of this in reality is negligible, so, let's see if we can find the exact date!

the unofficial google maps api

while google maps provides a somewhat useful api, it is missing quite a lot of features. luckily for us, however, there is an undocumented endpoint called SingleImageSearch (located at https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/SingleImageSearch), which you may find out about if you inspect the calls made by any website that embeds streetview.

this endpoint makes calls, and receives data in, a slightly weird json-encoded protobuf format commonly used by grpc applications. as such, we are faced with a problem: how do we figure out what parameters we can send, where do we send them, and how do we receive the data?

luckily, thanks to some experimentation by my amazing friend reanna, we actually have some good leads. it turns out that if we send bogus data to the endpoint, it will give us a helpful error message containing a number of important details:

to give one example: if we go on map-generator, we can find that an example of a valid request could be something like this:

[
	["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],
	[[null,null,49.75388487172678,6.635720976651974],500],
	[null,["en","GB"],null,null,null,null,null,null,[2],null,[[[2,1,2]]]],
	[[1,2,3,4,8,6]]
]

some of the values here could already be guessed somewhat easily: 49.75388487172678,6.635720976651974 is quite obviously the coordinates we are searching around, while ["en","GB"] is almost certainly language-related parameters of some sort. however, other values are less obvious. what happens, for example, if we edit one of the nulls, and send something like this instead:

[
	["apiv3","☭",null,null,"US",null,null,null,null,null,[[0]]],
	[[null,null,49.75388487172678,6.635720976651974],500],
	[null,["en","GB"],null,null,null,null,null,null,[2],null,[[[2,1,2]]]],
	[[1,2,3,4,8,6]]
]

it turns out that we are given the following error message as the response:

[
	3,
	"Invalid value at 'context.source' (type.googleapis.com/geo_photo_service.RequestContext.RequestSource), \"☭\"",
	[
		[
			"type.googleapis.com/google.rpc.BadRequest",
			[[[
				"context.source",
				"Invalid value at 'context.source' (type.googleapis.com/geo_photo_service.RequestContext.RequestSource), \"☭\""
			]]]
		]
	]
]

this is quite helpful: we now know that the value at index [0][1] is called context.source, and its type is geo_photo_service.RequestContext.RequestSource. since this is not among the list of known protobuf types, we can safely assume that it is either a message (roughly equivalent to a typescript interface), or an enum.

from here, we need to do two things: first, send a couple of (low) numbers to see if the type is an enum. if it is an enum, we will need to bomb it with values and see how the response changes to figure out what is what. if it is a message, however, then it is safe to continue the procedure one layer down (i.e. instead of sending "☭", we can send something like ["☭","☭","☭"]), all the way until we get to some known datatypes.

i will not bore you with the exact details, but if you repeat the steps of "send bogus data → see what the endpoint tells you it wants → expand on it" for long enough, you will eventually find a parameter called photo_age, which itself has start_seconds and end_seconds. if you then send a unix timestamp to both of these parameters, the endpoint will only return panos which fall within the specified date range.

using this knowledge, we can find the pano date using a handful of fairly trivial steps:

  1. get the month of the pano using the "proper" streetview api
  2. set 00:00 on the first of the month as our start date, and 23:59 at the end of the month as our end date
  3. perform a binary search: with each attempt, we narrow down our interval to half its previous size. if the endpoint still returns an image, we know that the actual pano date is within the given date range, while if it doesn't, it will be outside of it.

testing the code with a couple examples that have a known time (i.e. a clock is visible), we can see that it is indeed possible the right dates with a reasonable degree of accuracy (2-3 minutes in my experience):

a screenshot of a clock showing a time of 1:12, followed by my date resolver tool guessing a time of 1:15
a couple minutes off, but good enough for me

if you want to try out the tool for yourself, you can click here, or look at the source code here.

← blog | ← home