| Reference docs | OWASP-Web-Top-10-2025.md, OWASP-Mobile-Top-10-2024.md, OWASP-LLM-Top-10-2026.md |
| Files inspected | 373 (105 source files) |
| Static analysis | Semgrep 1.172.0 |
| Dynamic analysis | DAST active (nuclei, auth-probe, mobsfscan, garak) |
| LLM triage | Claude claude-sonnet-5 |
| Live targets | web: http://127.0.0.1:8000, mobile-api: http://127.0.0.1:8000, llm: http://127.0.0.1:8000/intake/start |
Executive Summary
Gymmate is a cross-platform fitness application combining a FastAPI/Python backend, a React Native/Expo mobile client with native Swift components, and an emerging LLM-driven feature set, giving it a broad attack surface spanning web, mobile, and AI domains. The confirmed findings skew toward configuration and identity weaknesses rather than deep code-level injection flaws: authentication is undermined by unverified self-registration issuing live sessions (A07 — Authentication Failures), the backend suffers from multiple instances of A02 — Security Misconfiguration including root-privileged containers, permissive CORS, and unauthenticated exposure of interactive API documentation, and there is a gap in A09 — Security Logging & Alerting Failures around login events. Mobile-side issues are comparatively minor, involving supply-chain hygiene (M2) and an ATS cleartext exception scoped to local networking (M8). No critical-severity or injection-class findings were confirmed in this pass, suggesting the application's core data-handling logic is relatively sound, but the accumulation of misconfiguration and authentication gaps materially lowers the bar for account takeover and unauthorized backend access. The overall risk profile is moderate: individually the findings are containable, but several compound (weak registration plus poor logging plus exposed docs) into a more exploitable chain than any single issue implies.
Most urgent risks
- HIGH F-001: Open self-registration issues a live session with no email verification
Strategic recommendation
The programme should prioritize closing the identity and configuration gaps before expanding LLM or mobile feature surface, since these are the areas most reachable by unauthenticated attackers and cheapest to remediate: enforce email/phone verification before session issuance, lock down CORS to named origins, drop container privileges to a non-root user, and gate or remove public access to API documentation endpoints. In parallel, establish baseline security logging and alerting for authentication events so that account-takeover attempts and misconfiguration abuse are detectable rather than silent. Mobile supply-chain and ATS findings are lower urgency and can be folded into routine dependency-hardening and release-build review cycles. As the LLM components mature, the team should get ahead of risk by threat-modeling prompt construction, tool invocation, and data exposure paths now, before agentic capabilities are added, so that the same misconfiguration-driven pattern seen on the web side does not repeat in the AI surface. A recurring configuration-as-code review and a lightweight secure-SDLC checkpoint for new endpoints would address the root cause common to most findings here: settings and defaults left unreviewed rather than deliberate design choices.
Scoring: risk score = 10 × critical + 5 × high + 2 × medium + 0.5 × low, capped at 100 (0 = no risk, 100 = catastrophic — higher is worse). Grades: A ≤ 10, B ≤ 25, C ≤ 40, D ≤ 60, otherwise F. Any CRITICAL finding is graded no better than D; any HIGH no better than B.
| OWASP category | SAST | DAST | Found |
|---|---|---|---|
| A01Broken Access Control | ✓ | ✓ | – |
| A02Security Misconfiguration | ✓ | ✓ | ✓ |
| A03Software Supply Chain Failures | ✓ | ✓ | – |
| A04Cryptographic Failures | ✓ | – | – |
| A05Injection | ✓ | ✓ | – |
| A06Insecure Design | ✓ | – | – |
| A07Authentication Failures | ✓ | ✓ | ✓ |
| A08Software or Data Integrity Failures | ✓ | ✓ | – |
| A09Security Logging & Alerting Failures | ✓ | – | ✓ |
| A10Mishandling of Exceptional Conditions | ✓ | – | – |
| OWASP category | SAST | DAST | Found |
|---|---|---|---|
| M1Improper Credential Usage | ✓ | ✓ | – |
| M2Inadequate Supply Chain Security | – | – | ✓ |
| M3Insecure Authentication / Authorization | ✓ | ✓ | – |
| M4Insufficient Input / Output Validation | ✓ | ✓ | – |
| M5Insecure Communication | ✓ | ✓ | – |
| M6Inadequate Privacy Controls | ✓ | ✓ | – |
| M7Insufficient Binary Protections | ✓ | ✓ | – |
| M8Security Misconfiguration | ✓ | ✓ | ✓ |
| M9Insecure Data Storage | ✓ | ✓ | – |
| M10Insufficient Cryptography | ✓ | – | – |
| OWASP category | SAST | DAST | Found |
|---|---|---|---|
| LLM01Prompt Injection | ✓ | ✓ | – |
| LLM02Sensitive Information Disclosure | ✓ | ✓ | – |
| LLM03Excessive Agency | ✓ | – | – |
| LLM04Supply Chain | ✓ | ✓ | – |
| LLM05Data and Model Poisoning | – | – | – |
| LLM06Unbounded Consumption | ✓ | – | – |
| LLM07Misinformation | – | ✓ | – |
| LLM08Hidden Context Exposure | ✓ | – | – |
| LLM09Vector and Embedding Weaknesses | – | – | – |
| LLM10Improper Output Handling | ✓ | ✓ | – |
Domain Breakdown
| Domain | Critical | High | Medium | Low | Total |
|---|---|---|---|---|---|
| Web Application (OWASP Top 10:2025) | 0 | 1 | 2 | 2 | 5 |
| Mobile Application (OWASP Mobile Top 10 2024) | 0 | 0 | 0 | 2 | 2 |
| LLM / AI Application (OWASP Top 10 for LLM Applications 2026) | 0 | 0 | 0 | 0 | 0 |
Findings by OWASP category
| OWASP category | Findings |
|---|---|
| A02 — Security Misconfiguration | 3 |
| A07 — Authentication Failures | 1 |
| A09 — Security Logging & Alerting Failures | 1 |
| M2 — Inadequate Supply Chain Security | 1 |
| M8 — Security Misconfiguration | 1 |
Findings Summary
| ID | Severity | OWASP category | Location |
|---|---|---|---|
| F-001 | HIGH | A07Authentication Failures | backend/auth.py:147 |
| F-002 | MEDIUM | A02Security Misconfiguration | Dockerfile:40 |
| F-003 | MEDIUM | A02Security Misconfiguration | backend/server.py:41 |
| F-004 | LOW | M2Inadequate Supply Chain Security | mobile/.npmrc:1 |
| F-005 | LOW | M8Security Misconfiguration | mobile/ios/GymmateDev/Info.plist:1 |
| F-006 | LOW | A09Security Logging & Alerting Failures | backend/auth.py:154 |
| F-007 | LOW | A02Security Misconfiguration | backend/server.py:35 |
Detailed Findings
F-001: Open self-registration issues a live session with no email verification
| Severity | HIGH |
| OWASP category | A07Authentication Failures (Web Application (OWASP Top 10:2025)) |
| Vulnerable file | backend/auth.py |
| Line(s) | 147–151 |
| Source | Correlated (Semgrep + auth-probe) |
| Detection | SAST + DAST (correlated) |
| Confidence | HIGH |
Vulnerable code
@router.post("/register")
def register(credentials: Credentials):
email = check(credentials)
body = call("/signup", {"email": email, "password": credentials.password})
return session_from(body)
Root cause
register() only validates email shape/password length client-side-equivalent (check()) and then forwards straight to Supabase /signup; when the project's email confirmation is off, session_from() returns a fully usable access_token immediately, with no rate limiting or verification step in front of it — confirmed live by an auth-probe that posted random throwaway credentials and got back a valid, immediately-usable JWT and refresh token.
Impact
An attacker can script unlimited account creation with disposable addresses, each minting a fresh authenticated session in one request; combined with usage.py's per-account $5 LLM budget, this lets an attacker cheaply and repeatedly re-roll free assistant/plan-generation credit, and more generally enables spam signups and credential-stuffing infrastructure with no friction.
Remediation
Enable Supabase's 'Confirm email' setting so /signup never returns tokens for an unverified address, add IP/email-based rate limiting on /auth/register, and consider CAPTCHA or another anti-automation control on the endpoint per the doc's guidance on abuse of high-value flows.
from time import time
from collections import defaultdict
from fastapi import HTTPException, Request
_attempts = defaultdict(list)
RATE_WINDOW = 3600
RATE_MAX = 5
def rate_limit(key: str):
now = time()
_attempts[key] = [t for t in _attempts[key] if now - t < RATE_WINDOW]
if len(_attempts[key]) >= RATE_MAX:
raise HTTPException(status_code=429, detail="too many signups, try again later")
_attempts[key].append(now)
@router.post("/register")
def register(credentials: Credentials, request: Request):
rate_limit(request.client.host)
email = check(credentials)
body = call("/signup", {"email": email, "password": credentials.password})
user = body.get("user") or {}
if not user.get("email_confirmed_at") and not body.get("access_token"):
return {"session": False, "user": account(user)}
return session_from(body)
POST of freshly-generated random credentials to /auth/register returned HTTP 200 with a valid access_token, refresh_token and 3600s expiry immediately — no email confirmation step was enforced by the deployment under test.
F-002: Backend container runs as root (no USER directive)
| Severity | MEDIUM |
| OWASP category | A02Security Misconfiguration (Web Application (OWASP Top 10:2025)) |
| Vulnerable file | Dockerfile |
| Line(s) | 40 |
| Source | Semgrep rule dockerfile.security.missing-user.missing-user |
| Detection | SAST (static) |
| Confidence | HIGH |
Vulnerable code
CMD ["sh", "-c", "exec uvicorn server:app --app-dir backend --host 0.0.0.0 --port ${PORT}"]
Root cause
The image never creates or switches to a non-root user, so uvicorn and the whole FastAPI process (which holds the Anthropic, Supabase, OpenAI and APNs credentials from .env) run as root inside the container — an insecure default left in place, matching the doc's 'a default left in place... a debug setting shipped' pattern.
Impact
If any other flaw in the stack (dependency RCE, deserialization, path traversal in the static file mounts) is ever exploited, the attacker inherits root inside the container immediately, simplifying container breakout and giving unrestricted access to every mounted secret and file (exercise-images, web-setup, templates, .cache).
Remediation
Create a dedicated unprivileged user in the image, chown the app directory to it, and add a USER directive before the CMD; keep pip installs in the root-owned build layer only.
RUN addgroup --system app && adduser --system --ingroup app app \
&& chown -R app:app /app
USER app
CMD ["sh", "-c", "exec uvicorn server:app --app-dir backend --host 0.0.0.0 --port ${PORT}"]
Not dynamically confirmed (static finding only).
F-003: Wildcard CORS policy on the FastAPI backend
| Severity | MEDIUM |
| OWASP category | A02Security Misconfiguration (Web Application (OWASP Top 10:2025)) |
| Vulnerable file | backend/server.py |
| Line(s) | 41–46 |
| Source | Semgrep rule python.fastapi.security.wildcard-cors.wildcard-cors |
| Detection | SAST (static) |
| Confidence | MEDIUM |
Vulnerable code
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
Root cause
allow_origins, allow_methods and allow_headers are all wildcarded on every route (including /auth/, /account/, /chat/*), matching the doc's bullet on 'a permissive Access-Control-Allow-Origin: * on authenticated APIs' — any web origin can issue cross-origin requests and read the JSON responses.
Impact
Any third-party page that can obtain or guess a bearer token (e.g. through an XSS elsewhere, a malicious browser extension, or a compromised dependency in the web preview) can call every account, chat and usage endpoint cross-origin without the browser blocking the response; it also removes any origin-based defence-in-depth for the web-setup client.
Remediation
Replace the wildcard with an explicit allow-list of known origins (the Expo web preview host, the production web-setup origin), restrict allow_methods/allow_headers to what is actually used, and leave allow_credentials off since it already is.
import os
ALLOWED_ORIGINS = [o.strip() for o in os.environ.get("ALLOWED_ORIGINS", "").split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS or ["http://localhost:8081"],
allow_credentials=False,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type", "X-Trial-Id"],
)
Not dynamically confirmed (static finding only).
F-004: Mobile package manager config lacks minimum release-age protection
| Severity | LOW |
| OWASP category | M2Inadequate Supply Chain Security (Mobile Application (OWASP Mobile Top 10 2024)) |
| Vulnerable file | mobile/.npmrc |
| Line(s) | 1 |
| Source | Semgrep rule package_managers.npm.npm-missing-minimum-release-age.npm-missing-minimum-release-age |
| Detection | SAST (static) |
| Confidence | MEDIUM |
Vulnerable code
legacy-peer-deps=true
Root cause
The project's .npmrc sets no min-release-age, so npm install can resolve a package version within minutes of publication — the same floating/unpinned-resolution pattern the doc warns about ('Floating version ranges... that silently pick up a compromised release').
Impact
A compromised maintainer account or hijacked package could publish a malicious version that CI or a developer machine installs immediately, before the community or registry has time to flag and pull it, and that code would then ship inside the signed mobile release artifact.
Remediation
Add min-release-age=7 to mobile/.npmrc; pin exact dependency versions and commit lockfiles; verify checksums; run dependency scanning in CI; vet new SDKs before adoption.
legacy-peer-deps=true
min-release-age=7
Not dynamically confirmed (static finding only).
F-005: App Transport Security exception permits local-network cleartext traffic
| Severity | LOW |
| OWASP category | M8Security Misconfiguration (Mobile Application (OWASP Mobile Top 10 2024)) |
| Vulnerable file | mobile/ios/GymmateDev/Info.plist |
| Line(s) | 1 |
| Source | Dynamic analysis (mobsfscan) |
| Detection | DAST (dynamic) |
| Confidence | MEDIUM |
Vulnerable code
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Root cause
NSAllowsLocalNetworking is enabled so the dev client can reach the Metro bundler and a LAN-hosted backend over plain HTTP, which mobsfscan flags as a Network Security Config / ATS exception; NSAllowsArbitraryLoads is correctly left false so this is scoped to local networks only, and it is confined to the GymmateDev target rather than the store build.
Impact
On a shared or hostile local network (public Wi-Fi, a compromised router), traffic between the dev client and the LAN backend could be intercepted or modified, but this is limited to development builds — the comment in app.config.js states production relies on HTTPS with no such exception.
Remediation
Confirm the production Info.plist carries no NSAllowsLocalNetworking/NSAllowsArbitraryLoads exception, keep this exception restricted to the development bundle identifier only, and prefer scoping any future ATS exceptions to specific domains via NSExceptionDomains rather than a blanket local-networking allowance.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<!-- Keep NSAllowsLocalNetworking only in the Debug/GymmateDev configuration;
ensure it is absent from the release Info.plist entirely. -->
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
mobsfscan statically confirmed the NSAllowsLocalNetworking=true ATS exception in the shipped Info.plist for the development target.
F-006: Login endpoint performs no authentication event logging
| Severity | LOW |
| OWASP category | A09Security Logging & Alerting Failures (Web Application (OWASP Top 10:2025)) |
| Vulnerable file | backend/auth.py |
| Line(s) | 154–162 |
| Source | Semgrep rule web.logging.python-auth-events-not-logged-hint |
| Detection | SAST (static) |
| Confidence | LOW |
Vulnerable code
@router.post("/login")
def login(credentials: Credentials):
email = check(credentials)
body = call(
"/token",
{"email": email, "password": credentials.password},
params={"grant_type": "password"},
)
return session_from(body)
Root cause
The login handler forwards credentials to the identity provider and returns a session but never records the attempt or its outcome, matching the doc's bullet that auditable events like logins are 'not logged at all'.
Impact
Credential-stuffing bursts, brute-force attempts, and successful account takeovers leave no application-level trace, so detection depends entirely on the upstream IdP's own logging, if any exists.
Remediation
Log every login attempt with outcome (success/failure), user identifier, and request metadata; ship logs off-host to a central store; define alert thresholds for failure spikes; never log raw passwords; add rate limiting on repeated failures.
@router.post("/login")
def login(credentials: Credentials):
email = check(credentials)
try:
body = call(
"/token",
{"email": email, "password": credentials.password},
params={"grant_type": "password"},
)
except AuthError:
logger.warning("login_failed", extra={"email": email})
raise
logger.info("login_succeeded", extra={"email": email})
return session_from(body)
Not dynamically confirmed (static finding only).
F-007: FastAPI interactive docs and OpenAPI schema exposed with no authentication
| Severity | LOW |
| OWASP category | A02Security Misconfiguration (Web Application (OWASP Top 10:2025)) |
| Vulnerable file | backend/server.py |
| Line(s) | 35 |
| Source | Correlated (Semgrep + nuclei) |
| Detection | SAST + DAST (correlated) |
| Confidence | MEDIUM |
Vulnerable code
app = FastAPI()
Root cause
FastAPI() is instantiated with its defaults, so /docs, /redoc and /openapi.json are all served publicly with no auth guard or docs_url=None override — an unnecessary developer feature left enabled in a production-facing deployment, and dynamically confirmed reachable at /docs, /redoc and /openapi.json.
Impact
Any unauthenticated visitor can enumerate the complete API surface (every route, request/response schema, including the auth, account, usage and voice endpoints), materially speeding up reconnaissance for further attacks such as the open-registration and CORS issues above.
Remediation
Disable docs_url/redoc_url/openapi_url outside of development (e.g. gated on an ENV variable), or place them behind an internal-only network path/auth dependency if they must remain available for API consumers.
import os
app = FastAPI(
docs_url="/docs" if os.environ.get("ENV") == "development" else None,
redoc_url="/redoc" if os.environ.get("ENV") == "development" else None,
openapi_url="/openapi.json" if os.environ.get("ENV") == "development" else None,
)
Nuclei confirmed all three unauthenticated endpoints live: /docs returned the Swagger UI page, /openapi.json returned the full 21KB API schema, and /redoc returned the ReDoc viewer — all with plain unauthenticated GET requests.
Dismissed Static-Analysis Findings
| Rule |
|---|
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:708 |
| innerHTML is assigned an empty string literal to clear the week-switch container before repopulation; no untrusted data reaches the sink. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:715 |
| innerHTML is assigned a static, hard-coded HTML literal with no interpolated variables; the actual week/label text is set afterwards via textContent. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:729 |
| innerHTML is assigned an empty string literal to clear the plan.week container before rendering; no untrusted data involved. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:763 |
| innerHTML is assigned a static template literal containing only fixed markup (span classes); no user-controlled data is interpolated into the string. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:954 |
| innerHTML is assigned a fixed, hard-coded string with no interpolated variables or user input. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:968 |
| innerHTML is assigned an empty string literal to clear the focus panel; subsequent content is built with textContent, not innerHTML. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:1168 |
| innerHTML is assigned an empty string literal to clear the chat log after a reset call; no untrusted data reaches the sink. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:1238 |
| innerHTML is assigned an empty string literal to clear the intake log; transcript entries are subsequently rendered via the say() helper which the codebase uses with textContent, not raw HTML. |
web.crypto.hardcoded-secret-key-js Location: mobile/store/auth.ts:7 |
| KEY_TOKENS is an AsyncStorage key name ('gymmate.auth'), not a cryptographic secret or signing key; no key material is embedded here. False positive on a string-literal-heuristic rule. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:231 |
| innerHTML is assigned a fixed, hard-coded markup string with no interpolated data; nothing attacker-controlled reaches it. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:250 |
| Static literal markup (three empty tags) with no user input; not exploitable. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:264 |
| innerHTML is set to the empty string to clear the log before rendering with textContent/appendChild elsewhere; no injection surface. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:371 |
| innerHTML is set to the empty string to clear a container in an error handler; no untrusted data assigned. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:538 |
| innerHTML is set to the empty string before rebuilding the strip's children; no untrusted data assigned. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:560 |
| innerHTML is a fixed static template of empty divs; actual label/value strings are written afterward via textContent, which auto-escapes. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:577 |
| innerHTML is a fixed static template for one empty div; the label is written afterward via textContent. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:605 |
| innerHTML is set to the empty string to clear the toggles container before re-render; no untrusted data assigned. |
web.injection.js-dom-xss-innerhtml Location: web-setup/app.js:693 |
| innerHTML is set to the empty string to clear the week skeleton before appending generated placeholder nodes; no untrusted data assigned. |
Scan Metadata
| Languages detected | typescript, python, swift, sql, javascript, c, html |
| Semgrep rulesets | web.yaml, mobile.yaml, llm.yaml, owasp-top-ten, secrets, typescript, python, swift |
| Semgrep results | 22 raw |
| Context files reviewed | 53 |
| Project type | hybrid |
| DAST status | active |
| DAST tools ran | nuclei, auth-probe, mobsfscan, garak |
| DAST findings (raw) | 5 |
| Correlation candidates | 1 |
| DAST artifacts | /Users/dmitryshishov/Desktop/Gymmate/gymmate-app/dast_artifacts |
| LLM model | claude-sonnet-5 |
| Token usage | input 164935, output 32753, cache read 0, cache write 94255 |
Analyst notes and warnings
Analyst notes
- Context source files were not re-supplied in this batch (marked as already reviewed earlier), so contextual review here is limited to what the Semgrep snippets and the single correlation candidate (C-001) reveal; F-002 is reported on that basis with reduced confidence since the full body of the /auth/register handler (lines ~140-150) was not visible, only its tail (line 151). The correlation candidate references a DAST record 'D-001' for a Nuclei auth-bypass probe, but no DAST findings were included in this batch, so detection is left as 'sast' rather than 'both'/'correlated' pending that evidence. All eight innerHTML Semgrep hits in web-setup/app.js were reviewed against surrounding code and are consistent false positives: the codebase's pattern is to clear a container with
.innerHTML = ""or set static markup, then populate actual dynamic content withtextContent, which is safe against DOM XSS. - Scope covered: backend FastAPI app (server.py, auth.py, assistant.py, usage.py, voice.py, apns.py, postgrest.py), the mobile Expo/React Native app and its native Swift lock-screen voice feature, the web-setup static client, and the two OWASP-relevant DAST batches (auth-probe, mobsfscan, nuclei). All 9 web.injection.js-dom-xss-innerhtml hits were manually verified against the surrounding snippet and are false positives — either fixed literal markup or clear-then-rebuild patterns; none interpolate untrusted data into innerHTML. The hardcoded-secret-key-js hit is a storage-key string constant, not a secret. Three DAST-only findings (swagger-api, openapi, redoc-api-docs) were merged into one A02 finding since they are one root cause (FastAPI default docs left enabled). The open-registration DAST finding was merged with the corresponding backend/auth.py register() handler per the supplied correlation candidate; note the semgrep rule id referenced in that candidate (web.logging.python-auth-events-not-logged-hint) was not present in the supplied Semgrep batch, so the merge is based on direct source review of auth.py rather than a specific semgrep hit. Not independently verified: whether Supabase 'Confirm email' is actually off in the production project (the DAST probe suggests it is, against whatever environment was under test) — recommend confirming project auth settings directly. The prompts.py assistant system prompt, tool definitions (tools.py) and library.py catalog constraints were reviewed for LLM01/LLM03/LLM08 risk and appear reasonably well-scoped (closed movement catalog, tool-mediated state changes, no secrets embedded in the system prompt), so no LLM findings are reported from this batch; a second pass with the tools.py source and the remainder of assistant.py/prompts.py (truncated in the provided context) is recommended to fully assess excessive-agency and hidden-context-exposure risk in the immersive voice/tool-calling path.
Warnings
- mobsfscan: suppressed 11 parse warning(s) in vendored/third-party files (node_modules, Pods, ...).
- Skipped 2 non-loopback backend API host(s) extracted from mobile config (getmentor.tech, gymmate-app-hjwm.onrender.com); pass --allow-remote to scan them.