Use with caution. Address findings before production.
Scanned 5/3/2026, 6:26:18 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
4
high
101
medium
5
low
MCP Server Information
Findings
This package presents significant security concerns with a C grade and safety score of 43/100, driven primarily by 101 medium-severity issues including 38 instances of ANSI escape injection vulnerabilities and 34 server configuration problems. The four high-severity findings, combined with resource exhaustion risks and insecure container image configurations, suggest the package requires substantial hardening before production use. Installation should be deferred until these issues are addressed, particularly the injection vulnerabilities and configuration gaps that could enable exploitation.
Dependencies
minimatch (3)
Scan Details
Want deeper analysis?
Fast scan found 24 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
24 of 24 findings
24 findings
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 122 | """Load saved cookies before visiting the target URL""" |
| 123 | cookie_file = Path(self.cookie_filename) |
| 124 | if cookie_file.exists(): |
| 125 | cookies = pickle.load(open(self.cookie_filename, "rb")) |
| 126 | for cookie in cookies: |
| 127 | self.driver.add_cookie(cookie) |
| 128 | else: |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 88 | # Parse XML content |
| 89 | try: |
| 90 | root = ET.fromstring(response.text) |
| 91 | |
| 92 | # Extract title |
| 93 | title = root.find('.//article-title') |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | --- |
| 2 | sidebar_position: 2 |
| 3 | --- |
| 4 | |
| 5 | # Advanced Usage |
| 6 | |
| 7 | This guide covers advanced usage scenarios and configurations for the GPT Researcher MCP Server. |
| 8 | |
| 9 | ## Custom Configuration |
| 10 | |
| 11 | You can customize the MCP server behavior by modifying various configuration parameters: |
| 12 | |
| 13 | ### Environment Variables |
| 14 | |
| 15 | Create a `.env` file with additional configuration options: |
| 16 | |
| 17 | ```bash |
| 18 | # Required API keys |
| 19 | OPENAI_API_KEY=your_openai_api_key |
| 20 | TAVILY_API_KEY=your_tavily_api_key |
| 21 | |
| 22 | # Optional configurations assuming using OpenAI |
| 23 | STRAT |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
TLS certificate verification is disabled on an outbound HTTP client. Any MITM in the network path can intercept and modify requests / responses โ credentials, tokens, and tool output flow over a channel with no integrity guarantee. Python requests / httpx: drop `verify=False`. If the peer is using a private CA, set `verify="/path/to/ca-bundle.pem"` or configure the system trust store. Node TS axios / fetch: drop `rejectUnauthorized: false` from the agent / `httpsAgent` options. Same private-CA
Evidence
| 42 | try: |
| 43 | if self.is_url(): |
| 44 | try: |
| 45 | response = requests.get(self.link, timeout=(5, 30), stream=True) |
| 46 | response.raise_for_status() |
| 47 | except requests.exceptions.SSLError: |
| 48 | import logging |
| 49 | logging.getLogger(__name__).warning( |
| 50 | f"SSL verification failed for {self.link}, retrying without verification" |
| 51 | ) |
| 52 | response = requests.get(se |
Remediation
Drop the verify-disable flag. If the peer presents a private CA: - Python: pass `verify="/path/to/ca.pem"` or trust the system store - Node: `new https.Agent({ ca: fs.readFileSync("ca.pem") })` - Go: load the CA via `x509.NewCertPool().AppendCertsFromPEM(...)` and set `tls.Config.RootCAs` Self-signed certificates: import the cert into the OS trust chain rather than disabling verification per-call.
`.env` file contains a credential-like variable name (API_KEY / TOKEN / SECRET / PASSWORD / PRIVATE_KEY / BEARER) assigned to what looks like a real value. `.env` files ship inside `git archive`, `docker build` contexts, and install tarballs โ any secret here leaks downstream (MCP Top-10 R9). Replace the value with a placeholder, rename the file to `.env.example`, and load the real value from a secret manager at runtime. If the credential was already committed, revoke it now (it is still in git
Evidence
| 1 | TOGETHER_API_KEY= |
| 2 | BING_API_KEY= |
| 3 | HELICONE_API_KEY= |
Remediation
Remove the credential from the `.env` file and replace with a template placeholder: OPENAI_API_KEY=<your-openai-key> OPENAI_API_KEY=${OPENAI_API_KEY} Rename the file to `.env.example` so humans know it is a template. Store the real value in a secret manager and inject it at runtime. If the credential has already been committed, revoke it immediately (git history still contains it).
minimatch==3.0.5 has 3 known CVEs [HIGH]: GHSA-23c5-xmqv-rm74, GHSA-3ppc-4f35-3m26, GHSA-7r86-cg39-jmmj. Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
hast-util-is-element==1.1.0 last released 1007 days ago (>730d) โ possible abandoned package
Remediation
Typosquat: verify you meant the popular package. If so, correct the spelling; if you truly intended the less-common name, suppress with an inline waiver. Stale release: check whether the package has a maintained fork or successor. If no patched release exists, vendor the code or migrate to an active alternative before the unmaintained code accrues unfixed CVEs.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | ############################################### |
| 2 | # 1) Dependencies layer |
| 3 | ############################################### |
| 4 | FROM node:18.17.0-alpine AS deps |
| 5 | WORKDIR /app |
| 6 | |
| 7 | # Copy only package manifest first for better layer caching |
| 8 | COPY package.json ./ |
| 9 | |
| 10 | # Install dependencies (no lock file present โ recommend adding one for reproducibility) |
| 11 | RUN npm install --legacy-peer-deps |
| 12 | |
| 13 | ############################################### |
| 14 | # 2) Builder layer โ builds Next.js (.next) |
| 15 | ################################## |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM python:3.11-slim |
| 2 | |
| 3 | WORKDIR /app |
| 4 | |
| 5 | # Copy requirements first to leverage Docker cache |
| 6 | COPY requirements.txt . |
| 7 | RUN pip install --no-cache-dir -r requirements.txt |
| 8 | |
| 9 | # Copy the rest of the application |
| 10 | COPY . . |
| 11 | |
| 12 | # Expose the port the app will run on |
| 13 | EXPOSE 8000 |
| 14 | |
| 15 | # Start the application |
| 16 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM node:18.17.0-alpine |
| 2 | WORKDIR /app |
| 3 | COPY ./package.json ./ |
| 4 | RUN npm install --legacy-peer-deps |
| 5 | COPY . . |
| 6 | CMD ["node", "index.js"] |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM node:18.17.0-alpine |
| 2 | WORKDIR /app |
| 3 | COPY ./package.json ./ |
| 4 | RUN npm install --legacy-peer-deps |
| 5 | RUN npm install -g nodemon |
| 6 | COPY . . |
| 7 | CMD ["nodemon", "index.js"] |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | ######################################################################## |
| 2 | # Stage 1: Frontend build |
| 3 | ######################################################################## |
| 4 | FROM node:slim AS frontend-builder |
| 5 | WORKDIR /app/frontend/nextjs |
| 6 | |
| 7 | # Copy package files and install dependencies |
| 8 | COPY frontend/nextjs/package.json frontend/nextjs/package-lock.json* ./ |
| 9 | RUN npm install --legacy-peer-deps |
| 10 | |
| 11 | # Copy the rest of the frontend application and build it |
| 12 | COPY frontend/nextjs/ ./ |
| 13 | RUN npm run build |
| 14 | |
| 15 | ######## |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM node:18.17.0-alpine |
| 2 | WORKDIR /app |
| 3 | COPY ./package.json ./ |
| 4 | RUN npm install --legacy-peer-deps |
| 5 | COPY . . |
| 6 | CMD ["npm", "run", "dev"] |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 96 | continue |
| 97 | |
| 98 | query = example['problem'] |
| 99 | print(f"\nEvaluating query: {query}") |
| 100 | try: |
| 101 | result = await evaluate_single_query(query, evaluator) |
| 102 | results.append(result) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 128 | } |
| 129 | } |
| 130 | } catch (error) { |
| 131 | console.error('Error parsing WebSocket message:', error, event.data); |
| 132 | } |
| 133 | }; |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 42 | Returns: |
| 43 | |
| 44 | """ |
| 45 | print("Searching with query {0}...".format(self.query)) |
| 46 | """Useful for general internet search queries using the Bing API.""" |
| 47 | |
| 48 | # Search the query |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 10 | print(f"Breadth: {progress.current_breadth}/{progress.total_breadth}") |
| 11 | print(f"Queries: {progress.completed_queries}/{progress.total_queries}") |
| 12 | if progress.current_query: |
| 13 | print(f"Current query: {progress.current_query}") |
| 14 | |
| 15 | # Initialize researcher with deep research type |
| 16 | researcher = GPTResearcher( |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 89 | if (this.socket.readyState === WebSocket.OPEN) { |
| 90 | this.socket.send(payload); |
| 91 | console.log('Message sent:', payload); |
| 92 | } else { |
| 93 | this.socket.onopen = () => { |
| 94 | this.socket.send(payload); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 50 | try { |
| 51 | body = await request.json(); |
| 52 | } catch (parseError) { |
| 53 | console.error('Error parsing request body:', parseError); |
| 54 | return NextResponse.json( |
| 55 | { error: 'Invalid JSON in request body' }, |
| 56 | { status: 400 } |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 276 | # Close the loop |
| 277 | if not new_loop.is_closed(): |
| 278 | new_loop.close() |
| 279 | except Exception: |
| 280 | pass # Ignore close errors |
| 281 | |
| 282 | # Run in a thread pool to avoid blocking the main event loop |
| 283 | with concurrent.futures.ThreadPoolExecutor() as executor: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 94 | logger.error(f"Error during WebSocket disconnection: {e}") |
| 95 | # Still try to close the connection if possible |
| 96 | try: |
| 97 | await websocket.close() |
| 98 | except Exception: |
| 99 | pass # If this fails too, there's nothing more we can do |
| 100 | |
| 101 | async def start_streaming(self, task, report_type, report_source, source_urls, document_urls, tone, websocket, headers=None, query_domains=[], mcp_enabled=False, mcp_strategy="fast", mcp_configs=[], max_search_resul |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 258 | except asyncio.TimeoutError: |
| 259 | logger.debug("Timeout during task cleanup, continuing...") |
| 260 | except Exception: |
| 261 | pass # Ignore other cleanup errors |
| 262 | except Exception: |
| 263 | pass # Ignore cleanup errors |
| 264 | finally: |
| 265 | try: |
| 266 | # Give the loop a moment to finish any final cleanup |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 1 | define(["exports"],function(t){"use strict";try{self["workbox:core:7.0.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.0.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class i extends r{constructor(t, |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 256 | ) |
| 257 | ) |
| 258 | except asyncio.TimeoutError: |
| 259 | logger.debug("Timeout during task cleanup, continuing...") |
| 260 | except Exception: |
| 261 | pass # Ignore other cleanup errors |
| 262 | except Exception: |
| 263 | pass # Ignore cleanup errors |
| 264 | finally: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
analyze_sentiment