From 62216e36c15f55d4ef6cb97313db3aa54fc77fe0 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 19 Feb 2026 23:47:56 +0200 Subject: [PATCH] Fix GHSL-2026-045_Wekan. Thanks to GHSL and xet7 ! --- models/lib/attachmentUrlValidation.js | 167 ++++++++++++++++++++++++++ models/trelloCreator.js | 12 ++ models/wekanCreator.js | 12 ++ 3 files changed, 191 insertions(+) create mode 100644 models/lib/attachmentUrlValidation.js diff --git a/models/lib/attachmentUrlValidation.js b/models/lib/attachmentUrlValidation.js new file mode 100644 index 000000000..a142719f4 --- /dev/null +++ b/models/lib/attachmentUrlValidation.js @@ -0,0 +1,167 @@ +import { Meteor } from 'meteor/meteor'; + +let dnsModule; +let netModule; +let lookupSync; + +if (Meteor.isServer) { + dnsModule = require('dns'); + netModule = require('net'); + lookupSync = Meteor.wrapAsync(dnsModule.lookup); +} + +const BLOCKED_HOSTNAMES = new Set([ + 'localhost', + 'localhost.localdomain', + 'ip6-localhost', + 'ip6-loopback', + '0', + '0.0.0.0', +]); + +const IPV4_RANGES = [ + ['0.0.0.0', '0.255.255.255'], + ['10.0.0.0', '10.255.255.255'], + ['100.64.0.0', '100.127.255.255'], + ['127.0.0.0', '127.255.255.255'], + ['169.254.0.0', '169.254.255.255'], + ['172.16.0.0', '172.31.255.255'], + ['192.0.0.0', '192.0.0.255'], + ['192.0.2.0', '192.0.2.255'], + ['192.168.0.0', '192.168.255.255'], + ['198.18.0.0', '198.19.255.255'], + ['198.51.100.0', '198.51.100.255'], + ['203.0.113.0', '203.0.113.255'], + ['224.0.0.0', '239.255.255.255'], + ['240.0.0.0', '255.255.255.255'], +].map(([start, end]) => ({ + start: ipv4ToInt(start), + end: ipv4ToInt(end), +})); + +function ipv4ToInt(ip) { + const parts = ip.split('.').map(part => parseInt(part, 10)); + if (parts.length !== 4 || parts.some(part => Number.isNaN(part))) { + return null; + } + return parts.reduce((acc, part) => (acc << 8) + part, 0) >>> 0; +} + +function isIpv4Blocked(ip) { + const value = ipv4ToInt(ip); + if (value === null) { + return true; + } + return IPV4_RANGES.some(range => value >= range.start && value <= range.end); +} + +function isIpv6Blocked(ip) { + const normalized = ip.split('%')[0].toLowerCase(); + if (normalized === '::' || normalized === '::1' || /^0(:0){1,7}$/.test(normalized)) { + return true; + } + if (normalized.startsWith('::ffff:')) { + const ipv4 = normalized.replace('::ffff:', ''); + return isIpv4Blocked(ipv4); + } + if (normalized.startsWith('2001:db8')) { + return true; + } + const firstGroupRaw = normalized.split(':')[0]; + const firstGroup = firstGroupRaw === '' ? '0' : firstGroupRaw; + const firstValue = parseInt(firstGroup, 16); + if (Number.isNaN(firstValue)) { + return true; + } + if (firstValue >= 0xfc00 && firstValue <= 0xfdff) { + return true; + } + if (firstValue >= 0xfe80 && firstValue <= 0xfebf) { + return true; + } + if (firstValue >= 0xff00) { + return true; + } + return false; +} + +function isIpBlocked(ip) { + if (!netModule) { + return false; + } + const version = netModule.isIP(ip); + if (version === 4) { + return isIpv4Blocked(ip); + } + if (version === 6) { + return isIpv6Blocked(ip); + } + return true; +} + +function resolveHostname(hostname) { + if (!lookupSync) { + return []; + } + try { + const results = lookupSync(hostname, { all: true }); + if (Array.isArray(results)) { + return results.map(result => result.address); + } + if (results && results.address) { + return [results.address]; + } + return []; + } catch (error) { + return null; + } +} + +export function validateAttachmentUrl(urlString) { + if (!urlString || typeof urlString !== 'string') { + return { valid: false, reason: 'Empty URL' }; + } + + let parsed; + try { + parsed = new URL(urlString); + } catch (error) { + return { valid: false, reason: 'Invalid URL format' }; + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + return { valid: false, reason: 'Only HTTP and HTTPS protocols are allowed' }; + } + + const hostname = parsed.hostname; + if (!hostname) { + return { valid: false, reason: 'Missing hostname' }; + } + + const lowerHostname = hostname.toLowerCase(); + if (BLOCKED_HOSTNAMES.has(lowerHostname) || lowerHostname.endsWith('.localhost')) { + return { valid: false, reason: 'Localhost is not allowed' }; + } + + if (!Meteor.isServer || !netModule) { + return { valid: true }; + } + + if (netModule.isIP(lowerHostname)) { + return isIpBlocked(lowerHostname) + ? { valid: false, reason: 'IP address is not allowed' } + : { valid: true }; + } + + const addresses = resolveHostname(lowerHostname); + if (!addresses || addresses.length === 0) { + return { valid: false, reason: 'Hostname did not resolve' }; + } + + const blockedAddress = addresses.find(address => isIpBlocked(address)); + if (blockedAddress) { + return { valid: false, reason: 'Resolved IP address is not allowed' }; + } + + return { valid: true }; +} diff --git a/models/trelloCreator.js b/models/trelloCreator.js index 71ad277ba..d68f5966d 100644 --- a/models/trelloCreator.js +++ b/models/trelloCreator.js @@ -22,6 +22,7 @@ import { calendar } from '/imports/lib/dateUtils'; import getSlug from 'limax'; +import { validateAttachmentUrl } from './lib/attachmentUrlValidation'; const DateString = Match.Where(function(dateAsString) { check(dateAsString, String); @@ -471,6 +472,17 @@ export class TrelloCreator { } }; if (att.url) { + const validation = validateAttachmentUrl(att.url); + if (!validation.valid) { + if (process.env.DEBUG === 'true') { + console.warn( + 'Blocked attachment URL during Trello import:', + validation.reason, + att.url, + ); + } + return; + } Attachments.load(att.url, opts, cb, true); } else if (att.file) { Attachments.insert(att.file, opts, cb, true); diff --git a/models/wekanCreator.js b/models/wekanCreator.js index 96e1f30fe..3d61338d1 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -21,6 +21,7 @@ import { calendar } from '/imports/lib/dateUtils'; import getSlug from 'limax'; +import { validateAttachmentUrl } from './lib/attachmentUrlValidation'; const DateString = Match.Where(function(dateAsString) { check(dateAsString, String); @@ -519,6 +520,17 @@ export class WekanCreator { } }; if (att.url) { + const validation = validateAttachmentUrl(att.url); + if (!validation.valid) { + if (process.env.DEBUG === 'true') { + console.warn( + 'Blocked attachment URL during Wekan import:', + validation.reason, + att.url, + ); + } + return; + } Attachments.load(att.url, opts, cb, true); } else if (att.file) { Attachments.insert(att.file, opts, cb, true);