add http denylist checks

This commit is contained in:
Henry 2025-07-30 17:04:01 +01:00
parent e8dac2048f
commit e497630714
6 changed files with 70 additions and 41 deletions

View File

@ -3,8 +3,7 @@ import axios, { AxiosRequestConfig, Method, ResponseType } from 'axios'
import FormData from 'form-data' import FormData from 'form-data'
import * as querystring from 'querystring' import * as querystring from 'querystring'
import { getCredentialData, getCredentialParam } from '../../../src/utils' import { getCredentialData, getCredentialParam } from '../../../src/utils'
import * as ipaddr from 'ipaddr.js' import { checkDenyList } from '../../../src/httpSecurity'
import dns from 'dns/promises'
class HTTP_Agentflow implements INode { class HTTP_Agentflow implements INode {
label: string 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<any> { async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const method = nodeData.inputs?.method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' const method = nodeData.inputs?.method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
const url = nodeData.inputs?.url as string const url = nodeData.inputs?.url as string
@ -332,7 +293,7 @@ class HTTP_Agentflow implements INode {
// Build final URL with query parameters // Build final URL with query parameters
const finalUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url const finalUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url
await this.checkDenyList(finalUrl) await checkDenyList(finalUrl)
// Prepare request config // Prepare request config
const requestConfig: AxiosRequestConfig = { const requestConfig: AxiosRequestConfig = {

View File

@ -1,6 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core' 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.` 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() finalUrl = url.toString()
} }
// Check if URL is allowed by security policy
await checkDenyList(finalUrl)
try { try {
const res = await fetch(finalUrl, { const res = await fetch(finalUrl, {
method: 'DELETE', method: 'DELETE',

View File

@ -1,6 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core' 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.` 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() finalUrl = url.toString()
} }
// Check if URL is allowed by security policy
await checkDenyList(finalUrl)
try { try {
const res = await fetch(finalUrl, { const res = await fetch(finalUrl, {
headers: requestHeaders headers: requestHeaders

View File

@ -1,6 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core' 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.` 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 ...this.headers
} }
// Check if URL is allowed by security policy
await checkDenyList(inputUrl)
const res = await fetch(inputUrl, { const res = await fetch(inputUrl, {
method: 'POST', method: 'POST',
headers: requestHeaders, headers: requestHeaders,

View File

@ -1,6 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core' 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.` 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 ...this.headers
} }
// Check if URL is allowed by security policy
await checkDenyList(inputUrl)
const res = await fetch(inputUrl, { const res = await fetch(inputUrl, {
method: 'PUT', method: 'PUT',
headers: requestHeaders, headers: requestHeaders,

View File

@ -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<void> {
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)
}
}
}