Files
upstream-mirror 30810eecd7
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
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

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 };
}
};