Fetch Metadata Middleware
raw JSON →The `fetch-metadata` package provides Node.js middleware designed for Express and Connect applications to enforce browser Fetch metadata request headers, such as `Sec-Fetch-Site`, `Sec-Fetch-Mode`, and `Sec-Fetch-Dest`. This middleware plays a crucial role in enhancing application security by helping to prevent common web vulnerabilities like Cross-Site Request Forgery (CSRF), Cross-Site Script Inclusion (XSSI), and information leakage attacks, as part of a defense-in-depth strategy. Currently at stable version 1.0.0, it offers a highly configurable API allowing developers to define granular policies for request origins, navigation types, and specific allowed paths. While a specific release cadence isn't published, its initial stable release suggests a focus on reliability for security-critical applications. Its key differentiator lies in its specific focus on these modern browser security headers, providing a ready-to-use solution for integrating these protections into existing Node.js web servers.
Common errors
error TypeError: fetchMetadata is not a function ↓
import fetchMetadata from 'fetch-metadata' for ESM modules, or const fetchMetadata = require('fetch-metadata').default for CommonJS environments (though the former is recommended). error Access Denied: Request blocked by security policy. ↓
onError callback. Adjust allowedFetchSites, disallowedNavigationRequests, or add the problematic path to allowedPaths configuration. Ensure your client-side requests are sending appropriate Fetch Metadata headers. error Request hangs indefinitely (no response from server). ↓
onError callback to either call response.status(statusCode).send(message) to terminate the request with an error, or next() if you wish to bypass the block and allow the request to proceed (e.g., for logging and allowing in specific cases). Warnings
gotcha Misconfiguring `allowedFetchSites` or `disallowedNavigationRequests` can unintentionally block legitimate requests, leading to application downtime or degraded user experience. Understand the implications of each `Sec-Fetch-*` header value before deployment. ↓
gotcha When providing a custom `onError` callback, it is crucial to ensure that the function either terminates the response (e.g., `response.end()`, `response.send()`) or explicitly calls `next()` to pass control to the next middleware. Failing to do so will cause the request to hang indefinitely. ↓
gotcha The `allowedPaths` configuration uses the `url-pattern` library for path matching, which might have subtle differences compared to Express's native path-to-regexp parsing. While it supports dynamic segments, be mindful of exact syntax for complex patterns. ↓
Install
npm install fetch-metadata yarn add fetch-metadata pnpm add fetch-metadata Imports
- fetchMetadata wrong
const fetchMetadata = require('fetch-metadata')correctimport fetchMetadata from 'fetch-metadata' - FetchMetadataOptions
import type { FetchMetadataOptions } from 'fetch-metadata'
Quickstart
import express from 'express';
import fetchMetadata from 'fetch-metadata';
const app = express();
const port = 3000;
// Apply the fetch-metadata middleware with custom configuration.
// It will allow 'same-origin', 'same-site', and 'none' (user interaction) requests.
// It will block navigation requests trying to embed 'object' or 'embed' elements.
app.use(
fetchMetadata({
allowedFetchSites: ['same-origin', 'same-site', 'none'],
disallowedNavigationRequests: ['object', 'embed'],
errorStatusCode: 403,
allowedPaths: [
'/public-data', // Allow access to this path regardless of fetch metadata
{ path: '/health-check', method: 'GET' } // Allow GET requests to /health-check
],
onError: (request, response, next, options) => {
console.warn(`[Fetch Metadata] Blocked request to ${request.url} from site ${request.headers['sec-fetch-site'] || 'N/A'}`);
response.status(options.errorStatusCode).send('Access Denied: Request blocked by security policy.');
// For testing or specific bypasses, you could call next() here to allow the request:
// next();
}
})
);
// Define a simple root route
app.get('/', (req, res) => {
res.send(`
<h1>Fetch Metadata Middleware Demo</h1>
<p>This server enforces Fetch Metadata Request Headers.</p>
<p>Try making requests from different origins or contexts using browser dev tools.</p>
<p>A fetch from 'same-origin' to /protected should succeed.</p>
<a href="/public-data">Public Data (always allowed)</a><br/>
<a href="/health-check">Health Check (GET allowed)</a>
<script>
fetch('/protected')
.then(response => response.text())
.then(text => console.log('GET /protected (same-origin):', text))
.catch(error => console.error('GET /protected (same-origin) failed:', error));
fetch('/public-data')
.then(response => response.text())
.then(text => console.log('GET /public-data (allowed path):', text))
.catch(error => console.error('GET /public-data (allowed path) failed:', error));
</script>
`);
});
// A protected route that relies on fetch metadata policies
app.get('/protected', (req, res) => {
res.send('You accessed a protected resource (Sec-Fetch-Site: same-origin/same-site/none allowed).');
});
// An explicitly allowed public route via 'allowedPaths'
app.get('/public-data', (req, res) => {
res.send('This is public data, accessible via allowedPaths config.');
});
// A health check route with a specific method allowed via 'allowedPaths'
app.get('/health-check', (req, res) => {
res.status(200).send('Service is healthy!');
});
// Start the server
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
console.log('To test: Open in browser, then try to fetch /protected from a different origin (e.g., using browser console or a separate HTML page on another domain).');
});