Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
import { isTruthy } from '@rocket.chat/tools';
|
|
|
|
import {
|
|
allowlistedIpResolved,
|
|
isIpInAnyRange,
|
|
isIpValid,
|
|
isValidDomain,
|
|
normalizeAllowlistEntry,
|
|
normalizeHostForAllowlistMatch,
|
|
nslookup,
|
|
parseIpv4WithPort,
|
|
} from './helpers';
|
|
|
|
export const parseSsrfAllowlist = (value: string): string[] => {
|
|
if (!value.trim()) {
|
|
return [];
|
|
}
|
|
return value
|
|
.split(/[\n,]/)
|
|
.map((entry) => entry.trim())
|
|
.filter(isTruthy);
|
|
};
|
|
|
|
function getEffectiveAllowlist(allowListRaw: string[]): string[] {
|
|
return allowListRaw.map(normalizeAllowlistEntry).filter((entry) => entry.length > 0);
|
|
}
|
|
|
|
/** Normalize allowList (string or string[]) to a single effective list. Parses raw string in this single place. */
|
|
function toEffectiveAllowlist(allowList?: string | string[]): string[] {
|
|
if (allowList === undefined || allowList === null) {
|
|
return [];
|
|
}
|
|
if (typeof allowList === 'string') {
|
|
return getEffectiveAllowlist(parseSsrfAllowlist(allowList));
|
|
}
|
|
return getEffectiveAllowlist(allowList);
|
|
}
|
|
|
|
function isInAllowlist(hostOrIp: string, port: string | undefined, allowlist: string[]): boolean {
|
|
if (allowlist.length === 0) {
|
|
return false;
|
|
}
|
|
const normalized = normalizeHostForAllowlistMatch(hostOrIp);
|
|
const withPort = port ? `${normalized}:${port}` : normalized;
|
|
return allowlist.some((entry) => entry === normalized || entry === withPort);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the URL is allowed by SSRF rules.
|
|
* @param input - URL or host to check
|
|
* @param allowList - Optional raw string (newline/comma-separated) or array of allowed hosts/IPs/CIDRs. Parsed inside.
|
|
*/
|
|
export const checkForSsrf = async (input: string, allowList?: string | string[]): Promise<boolean> => {
|
|
const result = await checkForSsrfWithIp(input, allowList);
|
|
return result.allowed;
|
|
};
|
|
|
|
/**
|
|
* SSRF check with resolved IP for pinning. allowList is optional; string is parsed in this single place.
|
|
*/
|
|
export const checkForSsrfWithIp = async (
|
|
input: string,
|
|
allowList?: string | string[],
|
|
): Promise<{ allowed: false } | { allowed: true; resolvedIp: string }> => {
|
|
const effectiveAllowlist = toEffectiveAllowlist(allowList);
|
|
let ipOrDomain: string;
|
|
let port: string | undefined;
|
|
let wasUrlParsed = false;
|
|
|
|
try {
|
|
const url = new URL(input);
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
return { allowed: false };
|
|
}
|
|
ipOrDomain = url.hostname;
|
|
port = url.port || undefined;
|
|
wasUrlParsed = true;
|
|
} catch {
|
|
const parsed = parseIpv4WithPort(input);
|
|
if (parsed) {
|
|
ipOrDomain = parsed.ip;
|
|
port = parsed.port;
|
|
} else {
|
|
ipOrDomain = input;
|
|
}
|
|
}
|
|
|
|
if (ipOrDomain.startsWith('[') && ipOrDomain.endsWith(']')) {
|
|
ipOrDomain = ipOrDomain.slice(1, -1);
|
|
}
|
|
|
|
const parsed = parseIpv4WithPort(ipOrDomain);
|
|
if (parsed) {
|
|
ipOrDomain = parsed.ip;
|
|
if (parsed.port && !port) {
|
|
port = parsed.port;
|
|
}
|
|
}
|
|
|
|
const ipValid = isIpValid(ipOrDomain);
|
|
const domainValid = isValidDomain(ipOrDomain);
|
|
|
|
if (!ipValid && !domainValid) {
|
|
return { allowed: false };
|
|
}
|
|
|
|
if (isInAllowlist(ipOrDomain, port, effectiveAllowlist)) {
|
|
if (ipValid) {
|
|
return { allowed: true, resolvedIp: allowlistedIpResolved(ipOrDomain, port, wasUrlParsed) };
|
|
}
|
|
|
|
if (domainValid) {
|
|
try {
|
|
const resolvedIp = await nslookup(ipOrDomain);
|
|
const resolvedIpWithPort = !wasUrlParsed && port ? `${resolvedIp}:${port}` : resolvedIp;
|
|
return { allowed: true, resolvedIp: resolvedIpWithPort };
|
|
} catch {
|
|
return { allowed: false };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ipValid && isIpInAnyRange(ipOrDomain)) {
|
|
return { allowed: false };
|
|
}
|
|
if (domainValid && /metadata\.google\.internal/i.test(ipOrDomain)) {
|
|
return { allowed: false };
|
|
}
|
|
|
|
if (!domainValid) {
|
|
const ipWithPort = !wasUrlParsed && port ? `${ipOrDomain}:${port}` : ipOrDomain;
|
|
return { allowed: true, resolvedIp: ipWithPort };
|
|
}
|
|
|
|
try {
|
|
const resolvedIp = await nslookup(ipOrDomain);
|
|
if (isIpInAnyRange(resolvedIp)) {
|
|
return { allowed: false };
|
|
}
|
|
|
|
const resolvedIpWithPort = !wasUrlParsed && port ? `${resolvedIp}:${port}` : resolvedIp;
|
|
return { allowed: true, resolvedIp: resolvedIpWithPort };
|
|
} catch {
|
|
return { allowed: false };
|
|
}
|
|
};
|