Is Transit Schedules safe?

Medium risk

Transit Schedules renders unsanitized remote HTML in the new tab page and sends search queries and geolocation data to transitschedules.net.

On first new tab load, the extension fetches HTML from transitschedules.net and injects it directly into the page without sanitization, running in the chrome-extension:// origin with access to browser APIs. Every search autocomplete keystroke is sent to transitschedules.net, and the user's city and state—resolved on each new tab via a MaxMind GeoIP lookup—are embedded in requests to transitschedules.net's weather and search endpoints.

Transit Schedulesv1.0.5Chrome Web Store
49Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Search box text is sent to Transit Schedules autocomplete

MediumunwantedCWE-200Dynamic sandbox

Text typed into Transit Schedules search boxes goes into a GET to transitschedules.net for autocomplete.

What actually happens

You did this

You type a search query into the extension's new-tab search box.

The same autocomplete behavior appears for the top search box and the main search box.

The extension did this

The extension puts that text into a request to transitschedules.net for suggestions.

If the local install-date flag is missing, a submitted search can also open a transitschedules.net search URL.

Data placed into Transit Schedules request URLs

  • Your typed search text
    train schedule

    The words you type into the search box are included in the autocomplete lookup, which can reveal what you're searching for.

  • Autocomplete request URL
    https://transitschedules.net/extension/public/autosuggest?q=train%20schedule

    The destination receives your typed text as the query-string value used for suggestions.

  • Submitted-search fallback URL
    https://transitschedules.net/extension/public/search?kw=train%20schedule

    When the local date flag is absent, submitting a search can open a Transit Schedules search page with the search text in the URL.

Captured request

GEThttps://transitschedules.net/extension/public/autosuggest?q=train%20schedule

No live autocomplete response was captured in the prior dynamic-analysis attempt; the shipped source constructs this GET request when search text is non-empty.

The code that does this

The shipped code sends non-empty search text to the autocomplete endpoint

Readable version

Top search box autocomplete handler

static/js/main.79a63e01.chunk.js
var de = function () {  countTop = 0;  0 !== A.length    ? r.a.getJSON("https://transitschedules.net/extension/public/autosuggest?q=" + encodeURIComponent(A), function (e) {      var t;      e.toString().includes("Server Error:") ||        (e.gossip.results.length > 0 && r()("#topbarsearch").val() ? r()(".search-border1").addClass("expand-search1") : r()(".search-border1").removeClass("expand-search1"),          r()("#topbarsearch").blur(function () {            r()(".search-border1").removeClass("expand-search1");          }),          ee(e.gossip.results),          r()("#topbarsearch").keydown(function (e) {            var n = r()(".pointerCursor");            if (40 === e.which) {              if (t) {                t.removeClass("active");                var s = t.next();                t = s.length > 0 ? s.addClass("active") : n.first().addClass("active");              } else t = n.first().addClass("active");              var A,                c,                a = document.getElementById("topbarsearch");              if (void 0 !== a) a.value = 0 === a.value.length ? "" : null !== (A = null === (c = t[0]) || void 0 === c ? void 0 : c.innerText) && void 0 !== A ? A : "";            } else if (38 === e.which) {              if (t) {                t.removeClass("active");                var i = t.prev();                t = i.length > 0 ? i.addClass("active") : n.last().addClass("active");              } else t = n.last().addClass("active");              var l,                o,                d = document.getElementById("topbarsearch");              if (void 0 !== d) d.value = 0 === d.value.length ? "" : null !== (l = null === (o = t[0]) || void 0 === o ? void 0 : o.innerText) && void 0 !== l ? l : "";            } else if (13 === e.which) {              if (countTop == 0) {                return;              } else {                countTop++;              }              const topSearch = document.getElementById("topbarsearch").value;              if (topSearch) {                chrome.storage.local.get(['date'], results => {                  if (results && results.date) {                    executeSearch(topSearch, 'chrome');                  } else {                    handlesuggestions(e, topSearch);                  }                });              }            }          }));    })    : ee([]);}

Main search box autocomplete effect

static/js/main.79a63e01.chunk.js
Object(s.useEffect)(  function () {    count = 0;    0 !== A.length      ? b.a        .get("https://transitschedules.net/extension/public/autosuggest?q=" + encodeURIComponent(A))        .then(function (e) {          var t,            n = e.data;          n.toString().includes("Server Error:") ||            (n.gossip.results.length > 0 && r()("#mainsearch").val() ? r()(".search-border").addClass("expand-search") : r()(".search-border").removeClass("expand-search"),              r()("#mainsearch").blur(function () {                r()(".search-border").removeClass("expand-search");              }),              Y(n.gossip.results),              r()("#mainsearch").keydown(function (e) {                var n = r()(".main_search_autocomplete_item");                if (40 === e.which)                  if (t) {                    t.removeClass("active");                    var s = t.next();                    s.length > 0                      ? (t = s.addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText)                      : (t = n.first().addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText);                  } else (t = n.first().addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText);                else if (38 === e.which)                  if (t) {                    t.removeClass("active");                    var A = t.prev();                    A.length > 0                      ? (t = A.addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText)                      : (t = n.last().addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText);                  } else (t = n.last().addClass("active"))[0] && (document.getElementById("mainsearch").value = 0 === document.getElementById("mainsearch").value.length ? "" : t[0].innerText);                else if (13 === e.which) {                  if (count === 0) {                    return;                  } else {                    count++;                  }                  const mainsearch = document.getElementById("mainsearch").value;                  if (mainsearch) {                    chrome.storage.local.get(['date'], results => {                      if (results && results.date) {                        executeSearch(mainsearch, 'chrome');                      } else {                        handlesuggestions(e, mainsearch);                      }                    });                  }                }              }));        })        .catch(function (e) {          return console.log(e);        })      : Y([]);  },  [A])

The code that does this

The submitted-search fallback opens a Transit Schedules search URL when the date flag is absent

Readable version

Missing-date branch in the top search submit path

static/js/main.79a63e01.chunk.js
const topSearch = document.getElementById("topbarsearch").value;if (topSearch) {  chrome.storage.local.get(['date'], results => {    if (results && results.date) {      executeSearch(topSearch, 'chrome');    } else {      handlesuggestions(e, topSearch);    }  });}

Search dispatch and Transit Schedules URL opener

static/js/main.79a63e01.chunk.js
function handleresponse(value, searchword) {  const response = value;  const browserlink = searchword;  if (response && browserlink) {    executeSearch(response, browserlink);  } else {    console.log('response is not define !')  }}function getlinkmethod() {  return 'transit-shedule'}function executeSearch(val, method = 'chrome') {  if (typeof val !== 'string' || !val.trim()) {    return;  }  const methods = {        chrome: {      func: TransitsheduleperformChromeSearch,      config: { log: true }    },    'transit-shedule': {      func: TransitsheduleApi,      config: { log: false }    }  };  const methodDetails = methods[method];  if (!methodDetails) {    return;  }  try {    if (val) {      methodDetails.func(val)    }  } catch (error) {    console.error('Error during search execution:', error);  }}function handlesuggestions(e, val) {  const value = val;  if (value) {    const searchword = getlinkmethod();    handleresponse(value, searchword);  } else {    console.log("value is not define !")  }}function TransitsheduleApi(val) {  try {    const anchor = document.createElement('a');    anchor.target = '_blank';    anchor.href = `${apiUrl}/extension/public/search?kw=${val}`;    document.body.appendChild(anchor);    anchor.click();    document.body.removeChild(anchor);  } catch (error) {    console.error('Unexpected error performing transit schedule:', error);  }}

External service receiving the search request

    • transitschedules.net

    Receives autocomplete requests that include typed search text in the q query parameter; also hosts the fallback search URL used when the local date flag is absent.

New tab resolves your city and state from your IP on every open

MediumunwantedCWE-359Dynamic sandbox

Transit Schedules replaces your new-tab page.

What actually happens

You did this

You open a new browser tab.

The extension has registered itself as your new-tab page (newtab.html), so this runs automatically with no click or prompt.

The extension did this

The extension resolves your city and state from your IP address.

It calls a third-party geolocation service that maps your public IP to a city and state, then stores and forwards that location.

The code that does this

The new-tab page resolves your location on load

Readable version

ip.js — runs on every new tab load

js/ip.js
let getIP = async () => {    await geoip2.city(async (res) => {        const city = await res.city.names['en']        const state = await res.subdivisions[0].names['en']        await localStorage.setItem('cityName', city)        await localStorage.setItem('stateName', state)    }, (onError) => { console.log(onError) })}await getIP()

Captured request

GEThttps://geoip-js.com/geoip/v2.1/city/me?referrer=chrome-extension://dcchknlbddobifongmdiomhboikhebmk

200 OK. JSON body resolving the caller's IP to a city/state. Observed during dynamic analysis returning city.names.en = 'Hove' with subdivisions data; values then written to localStorage.

What the geolocation response yields and where it goes

  • Your city
    Hove

    Your approximate city, derived from your public IP address. This is read on every new tab and stored on your device.

  • Your state / region
    England

    Your approximate state or region, derived from your public IP address, stored alongside the city.

  • Your IP address
    (your public IP)

    Your public IP is sent to the geolocation service, which uses it to determine your location.

What's stored on your device

Your resolved city and state are persisted in the new-tab page's localStorage so the weather widget can re-render them on later opens.

Location
browser localStorage on the extension's new-tab page
Contents
{  "cityName": "Hove",  "stateName": "England",  "firstTime_using_online_tools__": "true"}

Hosts that receive your IP or resolved location

    • geoip-js.com

    MaxMind GeoIP2 web service. Receives a request from your browser and resolves your public IP address to a city and state.

    • api.openweathermap.org

    Weather API. Receives your resolved city in the query string (q=<city>) to return local weather for the widget.

    • transitschedules.net

    Extension publisher's site. Your city and state are embedded in a weather link (city=<city>&state=<state>) on the new-tab page.

The code that does this

The resolved city is forwarded to two more parties

Readable version

City passed to OpenWeatherMap

static/js/main.79a63e01.chunk.js
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city},&APPID=3ea48c53439d343afd46c0268a5ab4e8&units=imperial`axios.get(url).then(res => { /* render weather */ })

City + state embedded in publisher link

static/js/main.79a63e01.chunk.js
<a href={`https://transitschedules.net/extension/public/weather?city=${encodeURIComponent(city)}&state=${encodeURIComponent(stateName)}`} target="_blank" rel="noreferrer">

What it can do

Permissions this extension asks for, as declared in version 1.0.5. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Run searches with your default search engine

    search

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Transit Schedules contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • api.openweathermap.orgwidely used

    Transit Schedules sends data to api.openweathermap.org. A widely used service: 33 other extensions we have analyzed send data here.

  • geoip-js.com

    Transit Schedules sends data to geoip-js.com. 5 other extensions we have analyzed send data here.

  • transitschedules.net

    Transit Schedules sends data to transitschedules.net. 2 other extensions we have analyzed send data here.

Updated 30 September 2026dcchknlbddobifongmdiomhboikhebmk