From e497630714a4b0e8ba558e62752764574c2da08d Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 30 Jul 2025 17:04:01 +0100 Subject: [PATCH] add http denylist checks --- .../components/nodes/agentflow/HTTP/HTTP.ts | 43 +-------------- .../nodes/tools/RequestsDelete/core.ts | 4 ++ .../nodes/tools/RequestsGet/core.ts | 4 ++ .../nodes/tools/RequestsPost/core.ts | 4 ++ .../nodes/tools/RequestsPut/core.ts | 4 ++ packages/components/src/httpSecurity.ts | 52 +++++++++++++++++++ 6 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 packages/components/src/httpSecurity.ts diff --git a/packages/components/nodes/agentflow/HTTP/HTTP.ts b/packages/components/nodes/agentflow/HTTP/HTTP.ts index 5a6920edb..960119d05 100644 --- a/packages/components/nodes/agentflow/HTTP/HTTP.ts +++ b/packages/components/nodes/agentflow/HTTP/HTTP.ts @@ -3,8 +3,7 @@ import axios, { AxiosRequestConfig, Method, ResponseType } from 'axios' import FormData from 'form-data' import * as querystring from 'querystring' import { getCredentialData, getCredentialParam } from '../../../src/utils' -import * as ipaddr from 'ipaddr.js' -import dns from 'dns/promises' +import { checkDenyList } from '../../../src/httpSecurity' class HTTP_Agentflow implements INode { label: string @@ -232,44 +231,6 @@ class HTTP_Agentflow implements INode { ] } - private isDeniedIP(ip: string, denyList: string[]): void { - const parsedIp = ipaddr.parse(ip) - for (const entry of denyList) { - if (entry.includes('/')) { - try { - const [range, _] = entry.split('/') - const parsedRange = ipaddr.parse(range) - if (parsedIp.kind() === parsedRange.kind()) { - if (parsedIp.match(ipaddr.parseCIDR(entry))) { - throw new Error('Access to this host is denied by policy.') - } - } - } catch (error) { - throw new Error(`isDeniedIP: ${error}`) - } - } else if (ip === entry) throw new Error('Access to this host is denied by policy.') - } - } - - private async checkDenyList(url: string) { - const httpDenyListString: string | undefined = process.env.HTTP_DENY_LIST - if (!httpDenyListString) return url - const httpDenyList = httpDenyListString.split(',').map((ip) => ip.trim()) - - const urlObj = new URL(url) - - const hostname = urlObj.hostname - - if (ipaddr.isValid(hostname)) { - this.isDeniedIP(hostname, httpDenyList) - } else { - const addresses = await dns.lookup(hostname, { all: true }) - for (const address of addresses) { - this.isDeniedIP(address.address, httpDenyList) - } - } - } - async run(nodeData: INodeData, _: string, options: ICommonObject): Promise { const method = nodeData.inputs?.method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' const url = nodeData.inputs?.url as string @@ -332,7 +293,7 @@ class HTTP_Agentflow implements INode { // Build final URL with query parameters const finalUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url - await this.checkDenyList(finalUrl) + await checkDenyList(finalUrl) // Prepare request config const requestConfig: AxiosRequestConfig = { diff --git a/packages/components/nodes/tools/RequestsDelete/core.ts b/packages/components/nodes/tools/RequestsDelete/core.ts index d753ca084..049091c09 100644 --- a/packages/components/nodes/tools/RequestsDelete/core.ts +++ b/packages/components/nodes/tools/RequestsDelete/core.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import fetch from 'node-fetch' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { checkDenyList } from '../../../src/httpSecurity' export const desc = `Use this when you need to execute a DELETE request to remove data from a website.` @@ -165,6 +166,9 @@ export class RequestsDeleteTool extends DynamicStructuredTool { finalUrl = url.toString() } + // Check if URL is allowed by security policy + await checkDenyList(finalUrl) + try { const res = await fetch(finalUrl, { method: 'DELETE', diff --git a/packages/components/nodes/tools/RequestsGet/core.ts b/packages/components/nodes/tools/RequestsGet/core.ts index 603da4ff2..179c4377f 100644 --- a/packages/components/nodes/tools/RequestsGet/core.ts +++ b/packages/components/nodes/tools/RequestsGet/core.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import fetch from 'node-fetch' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { checkDenyList } from '../../../src/httpSecurity' export const desc = `Use this when you need to execute a GET request to get data from a website.` @@ -165,6 +166,9 @@ export class RequestsGetTool extends DynamicStructuredTool { finalUrl = url.toString() } + // Check if URL is allowed by security policy + await checkDenyList(finalUrl) + try { const res = await fetch(finalUrl, { headers: requestHeaders diff --git a/packages/components/nodes/tools/RequestsPost/core.ts b/packages/components/nodes/tools/RequestsPost/core.ts index 3360dcc2e..e680b26ec 100644 --- a/packages/components/nodes/tools/RequestsPost/core.ts +++ b/packages/components/nodes/tools/RequestsPost/core.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import fetch from 'node-fetch' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { checkDenyList } from '../../../src/httpSecurity' export const desc = `Use this when you want to execute a POST request to create or update a resource.` @@ -126,6 +127,9 @@ export class RequestsPostTool extends DynamicStructuredTool { ...this.headers } + // Check if URL is allowed by security policy + await checkDenyList(inputUrl) + const res = await fetch(inputUrl, { method: 'POST', headers: requestHeaders, diff --git a/packages/components/nodes/tools/RequestsPut/core.ts b/packages/components/nodes/tools/RequestsPut/core.ts index da937058e..de9d19365 100644 --- a/packages/components/nodes/tools/RequestsPut/core.ts +++ b/packages/components/nodes/tools/RequestsPut/core.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import fetch from 'node-fetch' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { checkDenyList } from '../../../src/httpSecurity' export const desc = `Use this when you want to execute a PUT request to update or replace a resource.` @@ -126,6 +127,9 @@ export class RequestsPutTool extends DynamicStructuredTool { ...this.headers } + // Check if URL is allowed by security policy + await checkDenyList(inputUrl) + const res = await fetch(inputUrl, { method: 'PUT', headers: requestHeaders, diff --git a/packages/components/src/httpSecurity.ts b/packages/components/src/httpSecurity.ts new file mode 100644 index 000000000..d52825350 --- /dev/null +++ b/packages/components/src/httpSecurity.ts @@ -0,0 +1,52 @@ +import * as ipaddr from 'ipaddr.js' +import dns from 'dns/promises' + +/** + * Checks if an IP address is in the deny list + * @param ip - IP address to check + * @param denyList - Array of denied IP addresses/CIDR ranges + * @throws Error if IP is in deny list + */ +export function isDeniedIP(ip: string, denyList: string[]): void { + const parsedIp = ipaddr.parse(ip) + for (const entry of denyList) { + if (entry.includes('/')) { + try { + const [range, _] = entry.split('/') + const parsedRange = ipaddr.parse(range) + if (parsedIp.kind() === parsedRange.kind()) { + if (parsedIp.match(ipaddr.parseCIDR(entry))) { + throw new Error('Access to this host is denied by policy.') + } + } + } catch (error) { + throw new Error(`isDeniedIP: ${error}`) + } + } else if (ip === entry) { + throw new Error('Access to this host is denied by policy.') + } + } +} + +/** + * Checks if a URL is allowed based on HTTP_DENY_LIST environment variable + * @param url - URL to check + * @throws Error if URL hostname resolves to a denied IP + */ +export async function checkDenyList(url: string): Promise { + const httpDenyListString: string | undefined = process.env.HTTP_DENY_LIST + if (!httpDenyListString) return + + const httpDenyList = httpDenyListString.split(',').map((ip) => ip.trim()) + const urlObj = new URL(url) + const hostname = urlObj.hostname + + if (ipaddr.isValid(hostname)) { + isDeniedIP(hostname, httpDenyList) + } else { + const addresses = await dns.lookup(hostname, { all: true }) + for (const address of addresses) { + isDeniedIP(address.address, httpDenyList) + } + } +}