mirror of
https://github.com/wekan/wekan.git
synced 2026-02-21 07:24:07 +01:00
Fix GHSL-2026-045_Wekan.
Thanks to GHSL and xet7 !
This commit is contained in:
parent
73eb98c57a
commit
62216e36c1
3 changed files with 191 additions and 0 deletions
167
models/lib/attachmentUrlValidation.js
Normal file
167
models/lib/attachmentUrlValidation.js
Normal file
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue