feat: Add option to restrict credential usage in http request node (#17583)

This commit is contained in:
Jon
2025-08-28 17:35:14 +01:00
committed by GitHub
parent 93e08d8735
commit f7f70f241e
10 changed files with 585 additions and 16 deletions

View File

@@ -8,6 +8,7 @@ import type {
JsonObject,
IHttpRequestMethods,
IRequestOptions,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import {
NodeApiError,
@@ -15,6 +16,7 @@ import {
sleep,
removeCircularRefs,
NodeConnectionTypes,
isDomainAllowed,
} from 'n8n-workflow';
import type { Readable } from 'stream';
@@ -676,6 +678,51 @@ export class HttpRequestV1 implements INodeType {
const options = this.getNodeParameter('options', itemIndex, {});
const url = this.getNodeParameter('url', itemIndex) as string;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),
`Invalid URL: ${url}. URL must start with "http" or "https".`,
);
}
const checkDomainRestrictions = async (
credentialData: ICredentialDataDecryptedObject,
url: string,
credentialType?: string,
) => {
if (credentialData.allowedHttpRequestDomains === 'domains') {
const allowedDomains = credentialData.allowedDomains as string;
if (!allowedDomains || allowedDomains.trim() === '') {
throw new NodeOperationError(
this.getNode(),
'No allowed domains specified. Configure allowed domains or change restriction setting.',
);
}
if (!isDomainAllowed(url, { allowedDomains })) {
const credentialInfo = credentialType ? ` (${credentialType})` : '';
throw new NodeOperationError(
this.getNode(),
`Domain not allowed: This credential${credentialInfo} is restricted from accessing ${url}. ` +
`Only the following domains are allowed: ${allowedDomains}`,
);
}
} else if (credentialData.allowedHttpRequestDomains === 'none') {
throw new NodeOperationError(
this.getNode(),
'This credential is configured to prevent use within an HTTP Request node',
);
}
};
if (httpBasicAuth) await checkDomainRestrictions(httpBasicAuth, url);
if (httpDigestAuth) await checkDomainRestrictions(httpDigestAuth, url);
if (httpHeaderAuth) await checkDomainRestrictions(httpHeaderAuth, url);
if (httpQueryAuth) await checkDomainRestrictions(httpQueryAuth, url);
if (oAuth1Api) await checkDomainRestrictions(oAuth1Api, url);
if (oAuth2Api) await checkDomainRestrictions(oAuth2Api, url);
if (
itemIndex > 0 &&
(options.batchSize as number) >= 0 &&

View File

@@ -1,4 +1,5 @@
import type {
ICredentialDataDecryptedObject,
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
@@ -15,6 +16,7 @@ import {
sleep,
removeCircularRefs,
NodeConnectionTypes,
isDomainAllowed,
} from 'n8n-workflow';
import type { Readable } from 'stream';
@@ -721,6 +723,67 @@ export class HttpRequestV2 implements INodeType {
const options = this.getNodeParameter('options', itemIndex, {});
const url = this.getNodeParameter('url', itemIndex) as string;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),
`Invalid URL: ${url}. URL must start with "http" or "https".`,
);
}
const checkDomainRestrictions = async (
credentialData: ICredentialDataDecryptedObject,
url: string,
credentialType?: string,
) => {
if (credentialData.allowedHttpRequestDomains === 'domains') {
const allowedDomains = credentialData.allowedDomains as string;
if (!allowedDomains || allowedDomains.trim() === '') {
throw new NodeOperationError(
this.getNode(),
'No allowed domains specified. Configure allowed domains or change restriction setting.',
);
}
if (!isDomainAllowed(url, { allowedDomains })) {
const credentialInfo = credentialType ? ` (${credentialType})` : '';
throw new NodeOperationError(
this.getNode(),
`Domain not allowed: This credential${credentialInfo} is restricted from accessing ${url}. ` +
`Only the following domains are allowed: ${allowedDomains}`,
);
}
} else if (credentialData.allowedHttpRequestDomains === 'none') {
throw new NodeOperationError(
this.getNode(),
'This credential is configured to prevent use within an HTTP Request node',
);
}
};
if (httpBasicAuth) await checkDomainRestrictions(httpBasicAuth, url);
if (httpBearerAuth) await checkDomainRestrictions(httpBearerAuth, url);
if (httpDigestAuth) await checkDomainRestrictions(httpDigestAuth, url);
if (httpHeaderAuth) await checkDomainRestrictions(httpHeaderAuth, url);
if (httpQueryAuth) await checkDomainRestrictions(httpQueryAuth, url);
if (oAuth1Api) await checkDomainRestrictions(oAuth1Api, url);
if (oAuth2Api) await checkDomainRestrictions(oAuth2Api, url);
if (nodeCredentialType) {
try {
const credentialData = await this.getCredentials(nodeCredentialType, itemIndex);
await checkDomainRestrictions(credentialData, url, nodeCredentialType);
} catch (error) {
if (
error.message?.includes('Domain not allowed') ||
error.message?.includes('configured to prevent') ||
error.message?.includes('No allowed domains specified')
) {
throw error;
}
}
}
if (
itemIndex > 0 &&
(options.batchSize as number) >= 0 &&

View File

@@ -12,6 +12,7 @@ import type {
JsonObject,
IRequestOptions,
IHttpRequestMethods,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import {
BINARY_ENCODING,
@@ -21,6 +22,7 @@ import {
jsonParse,
removeCircularRefs,
sleep,
isDomainAllowed,
} from 'n8n-workflow';
import type { Readable } from 'stream';
@@ -180,6 +182,70 @@ export class HttpRequestV3 implements INodeType {
nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex) as string;
}
const url = this.getNodeParameter('url', itemIndex) as string;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),
`Invalid URL: ${url}. URL must start with "http" or "https".`,
);
}
const checkDomainRestrictions = async (
credentialData: ICredentialDataDecryptedObject,
url: string,
credentialType?: string,
) => {
if (credentialData.allowedHttpRequestDomains === 'domains') {
const allowedDomains = credentialData.allowedDomains as string;
if (!allowedDomains || allowedDomains.trim() === '') {
throw new NodeOperationError(
this.getNode(),
'No allowed domains specified. Configure allowed domains or change restriction setting.',
);
}
if (!isDomainAllowed(url, { allowedDomains })) {
const credentialInfo = credentialType ? ` (${credentialType})` : '';
throw new NodeOperationError(
this.getNode(),
`Domain not allowed: This credential${credentialInfo} is restricted from accessing ${url}. ` +
`Only the following domains are allowed: ${allowedDomains}`,
);
}
} else if (credentialData.allowedHttpRequestDomains === 'none') {
throw new NodeOperationError(
this.getNode(),
'This credential is configured to prevent use within an HTTP Request node',
);
}
};
if (httpBasicAuth) await checkDomainRestrictions(httpBasicAuth, url);
if (httpBearerAuth) await checkDomainRestrictions(httpBearerAuth, url);
if (httpDigestAuth) await checkDomainRestrictions(httpDigestAuth, url);
if (httpHeaderAuth) await checkDomainRestrictions(httpHeaderAuth, url);
if (httpQueryAuth) await checkDomainRestrictions(httpQueryAuth, url);
if (httpCustomAuth) await checkDomainRestrictions(httpCustomAuth, url);
if (oAuth1Api) await checkDomainRestrictions(oAuth1Api, url);
if (oAuth2Api) await checkDomainRestrictions(oAuth2Api, url);
if (nodeCredentialType) {
try {
const credentialData = await this.getCredentials(nodeCredentialType, itemIndex);
await checkDomainRestrictions(credentialData, url, nodeCredentialType);
} catch (error) {
if (
error.message?.includes('Domain not allowed') ||
error.message?.includes('configured to prevent') ||
error.message?.includes('No allowed domains specified')
) {
throw error;
}
}
}
const provideSslCertificates = this.getNodeParameter(
'provideSslCertificates',
itemIndex,
@@ -257,8 +323,6 @@ export class HttpRequestV3 implements INodeType {
responseFileName = response?.response?.outputPropertyName;
const url = this.getNodeParameter('url', itemIndex) as string;
const responseFormat = response?.response?.responseFormat || 'autodetect';
fullResponse = response?.response?.fullResponse || false;