cd /news/ai-tools/crawlforge-v5-0-0-security-correctne… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-96856] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=Β· neutral

CrawlForge v5.0.0: Security, Correctness, MCP Spec

CrawlForge MCP Server v5.0.0 addresses a critical SSRF vulnerability that allowed IP-literal URLs to bypass hostname-based guards, potentially exposing loopback, link-local, and cloud metadata endpoints. The update also includes 95 fixes across security, correctness, and MCP spec compliance, raising the Node.js requirement to >=20.16.0.

read9 min views1 publishedAug 14, 2026

http://2130706433/

is a valid URL. Your browser will happily resolve it to 127.0.0.1

, because the WHATWG URL parser normalizes decimal, hex (0x7f000001

), and octal integer forms into dotted-quad IPv4.

Our SSRF guard did not know that. It resolved hostnames through DNS and range-checked the resulting addresses β€” but Node never routes an IP literal through lookup

, so a URL whose host was already an IP sailed straight past the check. Loopback, link-local, cloud metadata: all reachable, in a server whose entire job is fetching URLs a model picked for you.

That is one bug out of the seven-phase internal audit that became ** CrawlForge MCP Server v5.0.0**. The unit suite went from

npm audit

went from | Phase | Theme | Headline result | |---|---|---| | 0 | Dependency currency | npm audit 16 vulns β†’ 4 moderate, zero code change | | 1 | Critical security | SSRF IP-literal bypass, OAuth token minting, secret leakage, billing | | 2 | Correctness | 52 fixes β€” including a crawl_deep rewrite | | 3 | Leaks and timeouts | 24 fixes β€” browser contexts, unbounded caches, real deadlines | | 4 | HTTP transport | 19 fixes β€” multi-session streamable HTTP, working prompts, webhook HMAC | | 5 | Dependency modernization | Node β‰₯ 20 floor, 0 npm audit vulnerabilities | | 6 | MCP spec adoption | Structured output, async tasks, tool whitelisting, registry server.json |

MCP protocol compliance held at 100.0% COMPLIANT, 0 errors at every phase gate.

engines.node

moved from >=18.0.0

to >=20.16.0

.

Node 18 hit end-of-life in April 2025, and 20.16 is the floor required by pdf-parse

2.4.5 β€” the maintained ESM rewrite we needed to clear the last audit findings. Our Dockerfile (node:20-alpine

) and CI (Node 22) already satisfied it.

That is the entire breaking surface. No tool schema, output shape, or credit cost changed, and the tool count stays at 27.

node --version   # must be >= 20.16.0

Read this phase if you run any MCP scraping server near a private network.

BEFORE: url -> parse -> DNS lookup -> ipBlocked(resolved)?  -> fetch
                          |
                          +--> IP literal? no lookup happens.
                               guard never runs. request goes out.

AFTER:  url -> parse -> ipBlocked(literal host)? --------+
                     -> DNS lookup -> ipBlocked(addrs)? -+-> fetch
                     -> per-connect check in the undici dispatcher
                        (catches every redirect hop too)

v5.0.0 runs ipBlocked()

on IP-literal hostnames at pre-flight and wraps the undici dispatcher's buildConnector

with a per-connect check, so a redirect hop straight to an internal address is blocked as well.

Three more guard fixes landed with it:

::ffff:127.0.0.1

and ::ffff:169.254.169.254

are normalized to their embedded IPv4 before range checks, in both default and strict modes. Kills the DNS-controlled AAAA-record bypass.BLOCKED_DOMAINS

was dead config.We also wired the guard into five paths that never had it: scrape_with_actions

(with a post-navigation page.url()

re-check that closes the page on a redirect into a blocked range β€” that was a Playwright internal-network read primitive), map_site

, process_document

PDF downloads, webhook delivery and health checks, and deep_research

webhook notifications.

/oauth/authorize

now requires proof of the operator's API key before issuing a code, with constant-time digest comparison. The anonymous register β†’ authorize β†’ token flow that minted operator-billed bearer tokens is closed. Secret leakage. Usage telemetry passes tool params through maskSecrets()

before the payload leaves the process β€” third-party API keys, auth headers, and webhook signing secrets no longer travel in plaintext. deep_research

stopped writing LLM API keys to Winston file logs.

Billing. A throw from the credit check itself now bills zero; the error-path half-charge only applies once the handler has actually started. checkCredits

distinguishes 401/403 (invalid or revoked key) from 5xx (grace window) instead of reporting both as "insufficient credits."

If you want the general version of this problem rather than our specific one, we wrote it up separately: SSRF in MCP servers.

This is the "passes smoke tests, returns misleading output" class β€” the one that never shows up as an error in your logs.

** crawl_deep is usable for real crawls again.** BFS child pages were awaited from inside an occupied queue slot, so the per-task queue timeout bounded the

Promise timed out

, and low concurrency settings (including concurrency: 1

) deadlocked outright. Both fixed.crawl_deep

's result-cache key now covers extract_content

, content length, include/exclude patterns, follow_external

, respect_robots

, concurrency

, domain filter, and session. map_site

's covers search

, domain filter, include_metadata

, and group_by_path

. Previously a cached call could contradict your parameters for a full hour-long TTL.Content-Type

header or <meta charset>

sniff) instead of always UTF-8. No more U+FFFD soup from ISO-8859-1 or Shift_JIS sites.options

schemas for extract_content

, summarize_content

, and analyze_content

now use .passthrough()

. Every documented option key was being stripped before it reached the handler β€” which is summarize_content

always returned the same 2-sentence fallback mislabeled extractive

. The extractive summarizer now actually runs, and summaryLength

changes the output.extract_links

resolves relative hrefs against the final page URL rather than the origin, honors <base href>

, and classifies protocol-relative links as external. The same fixes landed in scrape

's extractor, so the two finally agree.track_changes

similarity.search_web

scoring.ranking_weights

deep-merge over the defaults instead of replacing them wholesale, so no more NaN

final scores or silently disabled duplicate checks. The zero-result expansion retry is capped at one fallback instead of up to five billed backend searches. 24 findings in the class that only surfaces in long-running processes.

Browser lifecycle. Closing a Playwright page does not close its context β€” so every scrape_with_actions

call and every browser-rendered extract_content

leaked one context until shutdown. Contexts are now closed alongside their page, and a failed page.goto

(DNS error, timeout, blocked URL) tears down both instead of orphaning them.

Bounded caches. crawl_deep

destroys its per-crawl CacheManager

in a finally

. Previously N crawls permanently leaked N caches of up to 1,000 full HTML documents each β€” every one of them re-running a JSON.stringify

memory scan every 60 seconds, forever. Dropped instances are now GC-verified with a WeakRef

regression test.

Deadlines on every body read. The abort timer stays armed through the body stream, so timeout

finally covers a server that returns headers and then stalls. Chunk reassembly is single-pass β€” it was O(nΒ²), roughly 1.5 seconds of synchronous event-loop block on a 25 MB body. PDF downloads got a real AbortSignal.timeout

(the old timeout:

fetch-init option is silently ignored by undici).

One for Claude Desktop users: snapshot storage defaults to ~/.crawlforge/snapshots

instead of process.cwd()

. MCP clients launch the server with a working directory of /

, where every snapshot write silently failed.

If you deployed over npm run start:http

, it was worse than you thought. A single shared transport meant exactly one session ever existed, and any clean disconnect bricked /mcp

until you restarted the process.

Stateful mode now follows the SDK's documented per-session pattern β€” a Map<sessionId, {transport, server}>

with a fresh transport and cloned McpServer

per initialize

, disposal on DELETE, and a JSON-RPC 404 for unknown session IDs. Second concurrent client, reconnect after a network drop, DELETE then fresh initialize: all work now.

Also in Phase 4:

getting-started

prompt was argsSchema

overload, advertising a bogus required argument and failing every prompts/get

. The compliance suite now covers discovery and retrieval for all 6 prompts.data

sub-object was being signed, so standard receiver-side raw-body verification failed every single time.scrape

no longer inlines multi-megabyte base64 screenshots into the JSON result β€” it keeps metadata plus a crawlforge://screenshot/{id}

resource URI.With the Node 20 floor in place, we retired every abandoned dependency and took the security upgrades the old floor had blocked. 4 moderate β†’ 0.

Removed outright: node-cron

(unused after Phase 3 moved scheduling to setInterval

; removal cleared its vulnerable uuid

chain), @googleapis/customsearch

(unused β€” the Google adapter calls the REST endpoint directly), and node-summarizer

(abandoned since 2019; the extractive summarizer was rewritten as a compromise

-based Luhn-style word-frequency scorer with identical result shapes).

The upgrade that mattered most: ** pdf-parse 1.1.1 β†’ 2.4.5**.

PDFProcessor

was ported to the v2 class API, so the password

option now actually decrypts protected PDFs β€” v1 silently ignored it.On supply chain: this phase ran during the ChainDrop npm worm. Every install ran with --ignore-scripts

, every adopted version was publish-date-gated, and the full lockfile diff was cross-checked against public compromised-package lists with zero matches.

Structured output (MCP 2025-06-18). scrape

, map_site

, serp_rank

, search_web

, extract_structured

, and crawl_deep

declare an outputSchema

and return structuredContent

alongside the legacy JSON text. The schemas are permissive by design, so a legitimate result can never fail SDK output validation.

Async tasks. crawl_deep

, batch_scrape

, deep_research

, and agent

are registered with taskSupport: 'optional'

under the io.modelcontextprotocol/tasks

extension. Task-aware clients get a handle immediately and poll tasks/get

; clients without task support still get the synchronous result exactly as before. This is the fix for long crawls timing out inside a client's tool-call window.

Client-side tool selection. Two env vars let you expose a subset of the 27 tools and cut context bloat:

CRAWLFORGE_TOOLS=scrape,search_web,extract_content

CRAWLFORGE_TOOL_GROUPS=search,extract

Unset means all tools. Unknown names are ignored with a stderr warning, and batch_scrape

auto-enables get_batch_results

.

Protocol hygiene. Schemas advertised in JSON Schema 2020-12 instead of draft-07. tools/list

sorted deterministically for client prompt-cache stability. Invalid tool arguments come back as isError: true

tool results β€” which a calling model can self-correct from β€” rather than -32602

protocol errors. And server.json

is complete against the 2025-12-11 registry schema.

serp_rank

, the v4.10.0 also added server-level MCP instructions

: the server tells any connecting client to prefer CrawlForge tools over its own built-in web capabilities. It ships in the server binary, so every client picks it up on the next launch after upgrade β€” no re-init

. It is guidance, not enforcement; an MCP server cannot disable a client's built-in tools.

Nothing changed. All 27 tools are metered and require an API key, at 1-10 credits per call.

Plan Price Credits
Free $0 (no card) 1,000 one-time trial (does not reset)
Hobby $19/mo 5,000
Professional $99/mo 50,000
Business $399/mo 250,000

Every plan gets every tool. LLM extraction defaults to local Ollama, so you do not need an OpenAI or Anthropic key unless you opt in.

npm install -g crawlforge-mcp-server@latest   # or an /mcp reconnect

npm install -g crawlforge-mcp-server && npx crawlforge init

Because Phase 2 fixed tool behavior rather than tool contracts, your existing calls keep working β€” they just return correct results now.

Deferred rather than rushed: a hosted remote endpoint with OAuth, a keyless tier, scheduled monitoring as a service, persistent sessions, and PII redaction.

Writing up your own bugs is uncomfortable, but "advertised control, silently non-functional" is the single most common failure mode we found across all seven phases β€” and it is invisible from the outside. If you find a CrawlForge control that does not behave the way the docs claim, that is exactly the bug we want.

npm: crawlforge-mcp-server Β·

── more in #ai-tools 4 stories Β· sorted by recency
── more on @crawlforge 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/crawlforge-v5-0-0-se…] indexed:0 read:9min 2026-08-14 Β· β€”