io.github.JaviMaligno/vitamind
version 1.0.0
- scanned 2026-10-16
- source github.com/JaviMaligno/vitamind
- commit 6aedc0ae562e
- transport streamable-http
- files read 401
- methodology 0.2.0
- ruleset 0.1.1
No tag matching version 1.0.0 was found in this repository, so the scan read the default branch instead. The findings below describe that code, not necessarily the released version.
72measured on 99% of this axis
35 findings in test code are listed but not deducted; 5 vendored or minified paths were excluded before scanning; machine-generated bundles are not the server's own code; 1 file could not be parsed and was not scanned
94measured on 80% of this axis
0
100measured on 45% of this axis
14
The evidence
Every deduction below names the artefact that produced it. Nothing on this page was executed: the source was read, never run, and no running server was contacted.
Code Safety
Showing the 50 most severe of 70 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- i18n/client-messages.ts:127mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const picked: MessageTree = {}; for (const namespace of CLIENT_NAMESPACES) { if (namespace in messages) picked[namespace] = messages[namespace]; } return picked; - i18n/metadata.ts:16mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const languages: Record<string, string> = {}; for (const l of routing.locales) { languages[l] = `${SITE_URL}${getPathname({ href: pathname, locale: l })}`; } languages["x-default"] = `${SITE_URL}${getPathname({ href: pathname, locale: routing.defaultLocale })}`; - lib/city-routes.ts:67mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const index: Record<string, Record<string, string>> = {}; for (const locale of routing.locales) { index[locale] = {}; for (const city of BUILTIN_CITIES) { index[locale][localizedCitySlug(locale, baseSlug(city.id))] = city.id; - lib/content-fingerprint.ts:327mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const out: Record<string, string> = {}; for (const path of paths) { out[path] = readFileSync(join(process.cwd(), path), "utf8").replace(/\r\n/g, "\n"); } return out; - scripts/build-widgets.mjs:37mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const key of keys) { if (typeof ns[key] !== "string") throw new Error(`messages/${locale}.json: falta ${namespace}.${key}`); picked[key] = ns[key]; } byLocale[locale] = picked; - scripts/build-widgets.mjs:39mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
picked[key] = ns[key]; } byLocale[locale] = picked; } const out = `// Generated by scripts/build-widgets.mjs from messages/*.json. Do not edit. - scripts/seed-cities.ts:104mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const r = rows[i]; if (r.slug) { slugs[i] = r.slug; continue; } - scripts/seed-cities.ts:112mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
: base; taken.add(slug); slugs[i] = slug; } - app/api/push/notify/route.ts:80mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// Timeout so one stalled Open-Meteo call can't hang the whole cron run past // the function limit (subscriptions are processed sequentially). const res = await fetch(url, { signal: AbortSignal.timeout(8000) }).catch(() => null); if (!res || !res.ok) return []; const data = await res.json(); - app/api/revalidate-today/route.ts:109mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// Vercel cache key for these routes, so it would buy nothing and only // make the probe fetch a URL no reader ever requests. const res = await fetch(sample.url, { cache: "no-store", signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), - app/api/weather/route.ts:68mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const pooled = !(start && end) && url.origin + url.pathname === FORECAST_URL; upstreamUrl = url.toString(); const main = fetch(upstreamUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }); const radUrl = radiationUrl(url); const radiation = pooled - app/api/weather/route.ts:71mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const radUrl = radiationUrl(url); const radiation = pooled ? fetch(radUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }) .then((r) => { if (r.ok) return r.json(); - lib/mcp-tools.ts:688mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// with Open-Meteo's own UV as the fallback (lib/cloud-transmission.ts). upstreamUrl = url.toString(); const main = fetch(upstreamUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }); const radUrl = radiationUrl(url); const radiation = fetch(radUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }) - lib/mcp-tools.ts:690mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const main = fetch(upstreamUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }); const radUrl = radiationUrl(url); const radiation = fetch(radUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }) .then((r) => { if (r.ok) return r.json(); - lib/weather-range.ts:91mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
export const fetchWeatherRange: WeatherRangeFetcher = async (lat, lon, from, to) => { try { const res = await fetch(rangeUrl(lat, lon, from, to), { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), }); - scripts/cloud-backtest/analyze2.py:10mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
MIN_ELEV = 25 R = [] for r in csv.DictReader(open(os.path.join(S, "rows2.csv"))): e = float(r["elev"]) if e < MIN_ELEV: continue - scripts/cloud-backtest/cross.py:6mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
def load(f): return [r for r in json.load(f) if all(r[m] is not None for m in M5 + ["best_match"])] HERE = os.path.dirname(os.path.abspath(__file__)) DE, US = (load(open(os.path.join(HERE, f))) for f in ("dwd_rows.json", "surfrad_rows.json")) def fit(rows): X = np.array([[r[m] for m in M5] + [1] for r in rows]); y = np.array([r["gnd"] for r in rows]); return nnls(X, y)[0] - scripts/cloud-backtest/dwd.py:25mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
out["dwd" + sid] = {"name": name, "lat": lat, "lon": lon, "hourly": hourly} print(name, len(hourly), file=sys.stderr, flush=True) json.dump(out, open(os.path.join(S, "dwd.json"), "w")) - scripts/cloud-backtest/fetch2.py:4mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
import json, math, time, calendar, urllib.request, sys, csv, os S = os.path.dirname(os.path.abspath(__file__)) cities = json.load(open(os.path.join(S, "cities.json"))) MODELS = ["best_match","ukmo_seamless","meteofrance_seamless","ecmwf_ifs025","icon_seamless","gfs_seamless"] PERIODS = [("2024-09-01","2025-08-31"),("2025-09-01","2026-09-20")] - scripts/cloud-backtest/fetch2.py:10mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
err = None for a in range(6): try: return json.load(urllib.request.urlopen(url, timeout=120)) except Exception as e: err = e; time.sleep(5*(a+1)) raise err - scripts/cloud-backtest/fetch2.py:21mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
la = math.radians(lat) return math.degrees(math.asin(math.sin(la)*math.sin(dec) + math.cos(la)*math.cos(dec)*math.cos(ha))) out = open(os.path.join(S, "rows2.csv"), "w", newline=""); w = csv.writer(out) w.writerow(["city","lat","lon","time_utc","elev","obs"] + MODELS) for c in cities: - scripts/cloud-backtest/ground.py:16mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
def get(url): for a in range(5): try: return json.load(urllib.request.urlopen(url, timeout=120)) except Exception as e: err = e; time.sleep(5) raise err - scripts/cloud-backtest/ground.py:19mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
except Exception as e: err = e; time.sleep(5) raise err G = json.load(open(os.path.join(S, sys.argv[1]))) rows = [] for code, st in G.items(): - scripts/cloud-backtest/ground.py:43mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
rows.append(r) print(code, sum(1 for r in rows if r["st"] == code), file=sys.stderr, flush=True) json.dump(rows, open(os.path.join(S, sys.argv[1].replace(".json","_rows.json")), "w")) print("\n1) DOES THE SATELLITE RANK MODELS LIKE THE GROUND DOES? (day-ahead, sun > 25 deg, MAE of clear-sky index)") - scripts/cloud-backtest/surfrad.py:14mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
yy = day.strftime("%y"); jd = day.timetuple().tm_yday url = f"https://gml.noaa.gov/aftp/data/radiation/surfrad/{code}/{day.year}/{code}{yy}{jd:03d}.dat" try: txt = urllib.request.urlopen(url, timeout=60).read().decode("latin-1") except Exception: continue lines = txt.splitlines() - scripts/cloud-backtest/surfrad.py:34mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
out[code] = {"name": ST[code], "lat": meta[0], "lon": meta[1], "elev_m": meta[2], "hourly": hourly} print(f"{code}: {nf} files, {len(hourly)} hours", file=sys.stderr, flush=True) json.dump(out, open(os.path.join(S, "surfrad.json"), "w")) - scripts/cloud-backtest/uv_check.py:6mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
import numpy as np S = os.path.dirname(os.path.abspath(__file__)) rows = json.load(open(os.path.join(S, "surfrad_rows.json"))); G = json.load(open(os.path.join(S, "surfrad.json"))) M5 = ["ukmo_seamless","meteofrance_seamless","ecmwf_ifs025","icon_seamless","gfs_seamless"] om = {} - scripts/cloud-backtest/uv_check.py:6mcp-python-path-join-unvalidated (medium/low)
A filesystem path is assembled from a value that is not a literal and opened without confinement. os.path.join discards the base entirely when the second component is absolute, and neither join nor `/` resolves `..`.
import numpy as np S = os.path.dirname(os.path.abspath(__file__)) rows = json.load(open(os.path.join(S, "surfrad_rows.json"))); G = json.load(open(os.path.join(S, "surfrad.json"))) M5 = ["ukmo_seamless","meteofrance_seamless","ecmwf_ifs025","icon_seamless","gfs_seamless"] om = {} - scripts/cloud-backtest/uv_check.py:12mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
# previous-runs' uv_index_previous_day1 comes back null, so Open-Meteo's UV is # taken from the historical host: short lead, i.e. an ADVANTAGE to method A. h = json.load(urllib.request.urlopen(f"https://historical-forecast-api.open-meteo.com/v1/forecast?latitude={st['lat']}&longitude={st['lon']}&hourly=uv_index,uv_index_clear_sky&timezone=GMT&start_date=2025-09-01&end_date=2026-08-31", timeout=120))["hourly"] for t, u, c in zip(h["time"], h["uv_index"], h["uv_index_clear_sky"]): if u is not None and c and c > 0.5: om[(code, t)] = u / c - scripts/generate-icons.mjs:65mcp-js-path-join-unvalidated (medium/low)
A filesystem path is assembled from a non-literal and read or written without confinement. path.join does not resolve away `..`, and path.resolve discards the base entirely on an absolute second component.
for (const size of sizes) { const svg = createSunSVG(size); fs.writeFileSync(path.join(outDir, `icon-${size}.svg`), svg); console.log(`Created icon-${size}.svg`); } - scripts/ozone-sample.ts:66mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
url.searchParams.set("start_date", from); url.searchParams.set("end_date", to); const res = await fetch(url.toString(), { signal: AbortSignal.timeout(TIMEOUT_MS) }); if (!res.ok) throw new Error(`${res.status} ${(await res.text()).slice(0, 200)}`); return res.json(); - scripts/setup-cities-table.ts:54mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
console.log("Project:", PROJECT_REF); const res = await fetch(mgmtUrl, { method: "POST", headers: { - scripts/svg-to-png.mjs:10mcp-js-path-join-unvalidated (medium/low)
A filesystem path is assembled from a non-literal and read or written without confinement. path.join does not resolve away `..`, and path.resolve discards the base entirely on an absolute second component.
async function convert(svgFile, pngFile, size) { const svg = fs.readFileSync(path.join(iconsDir, svgFile)); await sharp(svg).resize(size, size).png().toFile(path.join(iconsDir, pngFile)); console.log(`${svgFile} -> ${pngFile} (${size}x${size})`); - scripts/sw.template.js:69mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
caches.match(request).then((cached) => { if (cached) return cached; return fetch(request).then((response) => { if (response.ok) { const clone = response.clone(); - scripts/sw.template.js:83mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// Pages: network-first, fallback to cache, then offline page event.respondWith( fetch(request) .then((response) => { if (response.ok) { - app/api/__tests__/push-notify-route.test.ts:53mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("fails closed when CRON_SECRET is not configured", async () => { vi.stubEnv("CRON_SECRET", ""); const res = await GET(request(URL_BASE, "Bearer anything")); expect(res.status).toBe(500); }); - app/api/__tests__/push-notify-route.test.ts:58mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("rejects a missing Authorization header", async () => { const res = await GET(request(URL_BASE)); expect(res.status).toBe(401); }); - app/api/__tests__/push-notify-route.test.ts:63mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("rejects a wrong bearer token", async () => { const res = await GET(request(URL_BASE, "Bearer wrong")); expect(res.status).toBe(401); expect(getAllSubscriptions).not.toHaveBeenCalled(); - app/api/__tests__/push-notify-route.test.ts:69mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("accepts the correct bearer token", async () => { const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(res.status).toBe(200); const body = await res.json(); - app/api/__tests__/push-notify-route.test.ts:78mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
describe("/api/push/notify force-test gating", () => { it("rejects force=true when PUSH_TEST_ALLOWED_ENDPOINT is not set", async () => { const res = await GET(request(`${URL_BASE}?force=true`, "Bearer test-secret")); expect(res.status).toBe(400); }); - app/api/__tests__/push-notify-route.test.ts:88mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
{ subscription: { endpoint: "https://push.example/another" } }, ]); const res = await GET(request(`${URL_BASE}?force=true`, "Bearer test-secret")); expect(res.status).toBe(200); const body = await res.json(); - app/api/__tests__/push-notify-route.test.ts:100mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("does not leak internal error detail when the store throws", async () => { getAllSubscriptions.mockRejectedValue(new Error("supabase: column secret_column does not exist")); const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(res.status).toBe(500); const body = await res.json(); - app/api/__tests__/push-notify-route.test.ts:160mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("sends to a subscriber whose local clock is inside the morning window", async () => { at("2026-06-21T08:00:00Z"); // 10:00 in Madrid const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(res.status).toBe(200); expect(claimNotification).toHaveBeenCalledWith("https://push.example/madrid", "2026-06-21"); - app/api/__tests__/push-notify-route.test.ts:169mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("does not send in the middle of the subscriber's night", async () => { at("2026-06-21T02:00:00Z"); // 04:00 in Madrid const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).not.toHaveBeenCalled(); expect(claimNotification).not.toHaveBeenCalled(); - app/api/__tests__/push-notify-route.test.ts:177mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
it("does not send after the subscriber's morning has passed", async () => { at("2026-06-21T16:00:00Z"); // 18:00 in Madrid await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).not.toHaveBeenCalled(); }); - app/api/__tests__/push-notify-route.test.ts:189mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
getAllSubscriptions.mockResolvedValue([madridSub({ tz: 1 })]); at("2026-06-21T11:30:00Z"); // 13:30 in Madrid — past the window await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).not.toHaveBeenCalled(); }); - app/api/__tests__/push-notify-route.test.ts:196mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
getAllSubscriptions.mockResolvedValue([madridSub({ timezone: undefined, tz: 2 })]); at("2026-06-21T08:00:00Z"); await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).toHaveBeenCalledTimes(1); }); - app/api/__tests__/push-notify-route.test.ts:217mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
getAllSubscriptions.mockResolvedValue([madridSub({ lastNotifiedOn: "2026-06-21" })]); at("2026-06-21T08:00:00Z"); const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(claimNotification).not.toHaveBeenCalled(); expect(sendNotification).not.toHaveBeenCalled(); - app/api/__tests__/push-notify-route.test.ts:226mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
getAllSubscriptions.mockResolvedValue([madridSub({ lastNotifiedOn: "2026-06-20" })]); at("2026-06-21T08:00:00Z"); await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).toHaveBeenCalledTimes(1); }); - app/api/__tests__/push-notify-route.test.ts:234mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
claimNotification.mockResolvedValue(false); at("2026-06-21T08:00:00Z"); const res = await GET(request(URL_BASE, "Bearer test-secret")); expect(sendNotification).not.toHaveBeenCalled(); expect(await res.json()).toMatchObject({ sent: 0, deferred: 1 });
Auth Posture
- authentication_model
auth enforcement present at a call site in the source (inbound token verification) and documented in the README; "refused without credentials" (`03` §4) established by reading the code rather than by connecting
- transport_security
declared endpoint(s) are HTTPS: https://getvitamind.app/api/mcp/mcp, https://getvitamind.app/api/mcp-auth/mcp; scored 80, not 100: `03` §4 reserves 100 for HTTPS *with HSTS*, and an HSTS header can only be seen by connecting, which `04` §9 does not do. This is an unverifiable band, not an observed absence.
- secret_handling
detect-secrets matched 56 credential-shaped string(s), every one from a keyword or entropy heuristic (Base64 High Entropy String, Hex High Entropy String, Secret Keyword). Those flag placeholders, variable names and content hashes as readily as credentials, so they do not establish the committed credential `03` §4's 0 asserts, and this sub-check is not scored; Hex High Entropy String at docs/analytics-results/2026-09-28-mcp-deployment.json:3; Hex High Entropy String at lib/content-revision.ts:263; Hex High Entropy String at lib/content-revision.ts:265; Hex High Entropy String at lib/content-revision.ts:266; Hex High Entropy String at lib/content-revision.ts:267; Hex High Entropy String at lib/content-revision.ts:268; Hex High Entropy String at lib/content-revision.ts:269; Hex High Entropy String at lib/content-revision.ts:308; Hex High Entropy String at lib/content-revision.ts:309; Hex High Entropy String at lib/content-revision.ts:310; …and 46 more matches
- authorization_granularity
9 tool registrations with conditional access logic present in the same source
Dependency Health
Showing the 50 most severe of 147 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- package-lock.jsonvitest 4.0.18 — GHSA-5xrq-8626-4rwp
direct, CVSS 9.8, fixed in 4.1.0
https://osv.dev/vulnerability/GHSA-5xrq-8626-4rwp - package-lock.jsonnext 16.1.6 — GHSA-2xp9-vwfh-vxw4
direct, CVSS 9.5, fixed in 16.3.3
https://osv.dev/vulnerability/GHSA-2xp9-vwfh-vxw4 - package-lock.jsonnext 16.1.6 — GHSA-p293-qw3h-jr36
direct, CVSS 9.0, fixed in 16.3.3
https://osv.dev/vulnerability/GHSA-p293-qw3h-jr36 - package-lock.jsonproxy-addr 2.0.7 — GHSA-jqcg-44mw-7w3h
transitive, CVSS 9.1, fixed in 2.0.8
https://osv.dev/vulnerability/GHSA-jqcg-44mw-7w3h - package-lock.jsonsharp 0.34.5 — GHSA-rgj7-g3m4-5g8c
direct, CVSS 8.9, fixed in 0.35.4
https://osv.dev/vulnerability/GHSA-rgj7-g3m4-5g8c - package-lock.jsonsharp 0.34.5 — GHSA-wq5f-xc86-pv6w
direct, CVSS 8.9, fixed in 0.35.5
https://osv.dev/vulnerability/GHSA-wq5f-xc86-pv6w - package-lock.jsonnext 16.1.6 — GHSA-c4j6-fc7j-m34r
direct, CVSS 8.6, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-c4j6-fc7j-m34r - package-lock.jsonnext 16.1.6 — GHSA-6gpp-xcg3-4w24
direct, CVSS 8.3, fixed in 16.2.11
https://osv.dev/vulnerability/GHSA-6gpp-xcg3-4w24 - package-lock.jsonnext 16.1.6 — GHSA-89xv-2m56-2m9x
direct, CVSS 8.3, fixed in 16.2.11
https://osv.dev/vulnerability/GHSA-89xv-2m56-2m9x - package-lock.jsonnext 16.1.6 — GHSA-cjq9-62q9-8jv4
direct, CVSS 8.3, fixed in 16.3.8
https://osv.dev/vulnerability/GHSA-cjq9-62q9-8jv4 - package-lock.jsonnext 16.1.6 — GHSA-p9j2-gv94-2wf4
direct, CVSS 8.3, fixed in 16.2.11
https://osv.dev/vulnerability/GHSA-p9j2-gv94-2wf4 - package-lock.jsonnext 16.1.6 — GHSA-m99w-x7hq-7vfj
direct, CVSS 8.2, fixed in 16.2.11
https://osv.dev/vulnerability/GHSA-m99w-x7hq-7vfj - package-lock.jsonnext 16.1.6 — GHSA-492v-c6pp-mqqv
direct, CVSS 8.1, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-492v-c6pp-mqqv - package-lock.jsonadm-zip 0.5.16 — GHSA-7q85-xj36-vmfc
direct, CVSS 7.5, fixed in 0.6.1
https://osv.dev/vulnerability/GHSA-7q85-xj36-vmfc - package-lock.jsonadm-zip 0.5.16 — GHSA-8238-w5pm-2374
direct, CVSS 7.5, fixed in 0.6.1
https://osv.dev/vulnerability/GHSA-8238-w5pm-2374 - package-lock.jsonadm-zip 0.5.16 — GHSA-rcw4-f5rp-g42v
direct, CVSS 7.5, fixed in 0.6.1
https://osv.dev/vulnerability/GHSA-rcw4-f5rp-g42v - package-lock.jsonadm-zip 0.5.16 — GHSA-xcpc-8h2w-3j85
direct, CVSS 7.5, fixed in 0.6.0
https://osv.dev/vulnerability/GHSA-xcpc-8h2w-3j85 - package-lock.jsonnext 16.1.6 — GHSA-267c-6grr-h53f
direct, CVSS 7.5, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-267c-6grr-h53f - package-lock.jsonnext 16.1.6 — GHSA-26hh-7cqf-hhc6
direct, CVSS 7.5, fixed in 16.2.6
https://osv.dev/vulnerability/GHSA-26hh-7cqf-hhc6 - package-lock.jsonnext 16.1.6 — GHSA-36qx-fr4f-26g5
direct, CVSS 7.5, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-36qx-fr4f-26g5 - package-lock.jsonnext 16.1.6 — GHSA-8h8q-6873-q5fj
direct, CVSS 7.5, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-8h8q-6873-q5fj - package-lock.jsonnext 16.1.6 — GHSA-mg66-mrh9-m8jx
direct, CVSS 7.5, fixed in 16.2.5
https://osv.dev/vulnerability/GHSA-mg66-mrh9-m8jx - package-lock.jsonnext 16.1.6 — GHSA-q4gf-8mx6-v5v3
direct, CVSS 7.5, fixed in 16.2.3
https://osv.dev/vulnerability/GHSA-q4gf-8mx6-v5v3 - package-lock.jsonpostcss 8.4.31 — GHSA-6g55-p6wh-862q
direct, CVSS 7.5, fixed in 8.5.12
https://osv.dev/vulnerability/GHSA-6g55-p6wh-862q - package-lock.jsonpostcss 8.5.8 — GHSA-6g55-p6wh-862q
direct, CVSS 7.5, fixed in 8.5.12
https://osv.dev/vulnerability/GHSA-6g55-p6wh-862q - package-lock.jsonpostcss 8.4.31 — GHSA-r28c-9q8g-f849
direct, CVSS 7.5, fixed in 8.5.18
https://osv.dev/vulnerability/GHSA-r28c-9q8g-f849 - package-lock.jsonpostcss 8.5.8 — GHSA-r28c-9q8g-f849
direct, CVSS 7.5, fixed in 8.5.18
https://osv.dev/vulnerability/GHSA-r28c-9q8g-f849 - package-lock.jsonadm-zip 0.5.16 — GHSA-j5f4-cc29-5x44
direct, CVSS 7.1, fixed in 0.6.1
https://osv.dev/vulnerability/GHSA-j5f4-cc29-5x44 - package-lock.jsonsharp 0.34.5 — GHSA-f88m-g3jw-g9cj
direct, CVSS 7.0, fixed in 0.35.0
https://osv.dev/vulnerability/GHSA-f88m-g3jw-g9cj - package-lock.jsonbraces 3.0.3 — GHSA-vfj7-8cjw-p6xm
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-vfj7-8cjw-p6xm - package-lock.jsonsource-map-js 1.2.1 — GHSA-68fv-2mgg-jv7q
transitive, CVSS 8.7, fixed in 1.2.2
https://osv.dev/vulnerability/GHSA-68fv-2mgg-jv7q - package-lock.jsonnanoid 3.3.11 — GHSA-28wg-ghj8-5hjv
transitive, CVSS 8.2, fixed in 3.3.16
https://osv.dev/vulnerability/GHSA-28wg-ghj8-5hjv - package-lock.jsonnanoid 3.3.11 — GHSA-2v37-7h3g-55p8
transitive, CVSS 8.2, fixed in 3.3.18
https://osv.dev/vulnerability/GHSA-2v37-7h3g-55p8 - package-lock.jsonvite 7.3.1 — GHSA-fx2h-pf6j-xcff
transitive, CVSS 8.2, fixed in 7.3.5
https://osv.dev/vulnerability/GHSA-fx2h-pf6j-xcff - package-lock.jsonvite 7.3.1 — GHSA-p9ff-h696-f583
transitive, CVSS 8.2, fixed in 7.3.2
https://osv.dev/vulnerability/GHSA-p9ff-h696-f583 - package-lock.jsonvite 7.3.1 — GHSA-v2wj-q39q-566r
transitive, CVSS 8.2, fixed in 7.3.2
https://osv.dev/vulnerability/GHSA-v2wj-q39q-566r - package-lock.jsonbrace-expansion 1.1.14 — GHSA-3jxr-9vmj-r5cp
transitive, CVSS 7.7, fixed in 1.1.16
https://osv.dev/vulnerability/GHSA-3jxr-9vmj-r5cp - package-lock.jsonbrace-expansion 5.0.5 — GHSA-3jxr-9vmj-r5cp
transitive, CVSS 7.7, fixed in 5.0.7
https://osv.dev/vulnerability/GHSA-3jxr-9vmj-r5cp - package-lock.jsonip-address 10.2.0 — GHSA-mwp4-54f8-5fhr
transitive, CVSS 7.7, fixed in 10.3.1
https://osv.dev/vulnerability/GHSA-mwp4-54f8-5fhr - package-lock.json@modelcontextprotocol/sdk 1.26.0 — GHSA-6qxp-vccf-f47h
transitive, CVSS 7.5, fixed in 1.31.0
https://osv.dev/vulnerability/GHSA-6qxp-vccf-f47h - package-lock.jsonbrace-expansion 1.1.14 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 1.1.19
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - package-lock.jsonbrace-expansion 5.0.5 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 5.0.10
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - package-lock.jsonbrace-expansion 1.1.14 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 1.1.17
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - package-lock.jsonbrace-expansion 5.0.5 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 5.0.8
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - package-lock.jsonbrace-expansion 1.1.14 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 1.1.20
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - package-lock.jsonbrace-expansion 5.0.5 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 5.0.11
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - package-lock.jsonbrace-expansion 1.1.14 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 1.1.18
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - package-lock.jsonbrace-expansion 5.0.5 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 5.0.9
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - package-lock.jsonbrowserslist 4.28.2 — GHSA-73wf-gq98-2v4g
transitive, CVSS 7.5, fixed in 4.28.7
https://osv.dev/vulnerability/GHSA-73wf-gq98-2v4g - package-lock.jsonbrowserslist 4.28.2 — GHSA-c83g-rgw3-j3cx
transitive, CVSS 7.5, fixed in 4.28.7
https://osv.dev/vulnerability/GHSA-c83g-rgw3-j3cx
Maintenance
- recency
last commit 14 day(s) before 2026-10-16
- issue_responsiveness
every issue read was filed by the project itself (13 by its maintainers or bots), so none is someone outside waiting for an answer, and `03` §5's question — will anyone read an issue I file? — has nothing to measure.
- release_cadence
this repository publishes neither releases nor version tags
- bus_factor
3 authors with >= 5 commits in 12 months
- repository_signals
`03` §5 scores this by quartile WITHIN the server's category, which needs a distribution across every scanned server and so cannot be computed while scanning one. It is a post-pass over the crawl and that pass does not exist yet.
Transparency
- license
package.json declares 'MIT' but no LICENSE file ships with the source
- readme_quality
README.md: configuration documented (+25)
- declared_scopes
neither the README nor the MCP manifest documents what the server accesses or what side effects it has
- changelog
no CHANGELOG in the fetched source. `03` §7's partial credit for tagged releases carrying release notes is not applied here — that needs the forge API, which this check does not call
- security_contact
no SECURITY.md and no disclosure contact in the README, so a finder has no route to report a vulnerability privately
Contest a finding
Every number here comes from a rule reading published source, and rules get things wrong. If something on this page is incorrect — or correct but mitigated by a check the rule cannot see — open an issue and we answer it in writing. A finding we got wrong is removed and the rule is fixed; one that stands is answered on the record.
Anyone can file: the maintainer, a researcher who disagrees with the classification, or a user who tested the server and could not reproduce it. Name this server and the rule or advisory ID shown beside the finding above. We aim to reply within five business days and to resolve within ten.