Skip to content

Commit 62216e3

Browse files
committed
Fix GHSL-2026-045_Wekan.
Thanks to GHSL and xet7 !
1 parent 73eb98c commit 62216e3

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Meteor } from 'meteor/meteor';
2+
3+
let dnsModule;
4+
let netModule;
5+
let lookupSync;
6+
7+
if (Meteor.isServer) {
8+
dnsModule = require('dns');
9+
netModule = require('net');
10+
lookupSync = Meteor.wrapAsync(dnsModule.lookup);
11+
}
12+
13+
const BLOCKED_HOSTNAMES = new Set([
14+
'localhost',
15+
'localhost.localdomain',
16+
'ip6-localhost',
17+
'ip6-loopback',
18+
'0',
19+
'0.0.0.0',
20+
]);
21+
22+
const IPV4_RANGES = [
23+
['0.0.0.0', '0.255.255.255'],
24+
['10.0.0.0', '10.255.255.255'],
25+
['100.64.0.0', '100.127.255.255'],
26+
['127.0.0.0', '127.255.255.255'],
27+
['169.254.0.0', '169.254.255.255'],
28+
['172.16.0.0', '172.31.255.255'],
29+
['192.0.0.0', '192.0.0.255'],
30+
['192.0.2.0', '192.0.2.255'],
31+
['192.168.0.0', '192.168.255.255'],
32+
['198.18.0.0', '198.19.255.255'],
33+
['198.51.100.0', '198.51.100.255'],
34+
['203.0.113.0', '203.0.113.255'],
35+
['224.0.0.0', '239.255.255.255'],
36+
['240.0.0.0', '255.255.255.255'],
37+
].map(([start, end]) => ({
38+
start: ipv4ToInt(start),
39+
end: ipv4ToInt(end),
40+
}));
41+
42+
function ipv4ToInt(ip) {
43+
const parts = ip.split('.').map(part => parseInt(part, 10));
44+
if (parts.length !== 4 || parts.some(part => Number.isNaN(part))) {
45+
return null;
46+
}
47+
return parts.reduce((acc, part) => (acc << 8) + part, 0) >>> 0;
48+
}
49+
50+
function isIpv4Blocked(ip) {
51+
const value = ipv4ToInt(ip);
52+
if (value === null) {
53+
return true;
54+
}
55+
return IPV4_RANGES.some(range => value >= range.start && value <= range.end);
56+
}
57+
58+
function isIpv6Blocked(ip) {
59+
const normalized = ip.split('%')[0].toLowerCase();
60+
if (normalized === '::' || normalized === '::1' || /^0(:0){1,7}$/.test(normalized)) {
61+
return true;
62+
}
63+
if (normalized.startsWith('::ffff:')) {
64+
const ipv4 = normalized.replace('::ffff:', '');
65+
return isIpv4Blocked(ipv4);
66+
}
67+
if (normalized.startsWith('2001:db8')) {
68+
return true;
69+
}
70+
const firstGroupRaw = normalized.split(':')[0];
71+
const firstGroup = firstGroupRaw === '' ? '0' : firstGroupRaw;
72+
const firstValue = parseInt(firstGroup, 16);
73+
if (Number.isNaN(firstValue)) {
74+
return true;
75+
}
76+
if (firstValue >= 0xfc00 && firstValue <= 0xfdff) {
77+
return true;
78+
}
79+
if (firstValue >= 0xfe80 && firstValue <= 0xfebf) {
80+
return true;
81+
}
82+
if (firstValue >= 0xff00) {
83+
return true;
84+
}
85+
return false;
86+
}
87+
88+
function isIpBlocked(ip) {
89+
if (!netModule) {
90+
return false;
91+
}
92+
const version = netModule.isIP(ip);
93+
if (version === 4) {
94+
return isIpv4Blocked(ip);
95+
}
96+
if (version === 6) {
97+
return isIpv6Blocked(ip);
98+
}
99+
return true;
100+
}
101+
102+
function resolveHostname(hostname) {
103+
if (!lookupSync) {
104+
return [];
105+
}
106+
try {
107+
const results = lookupSync(hostname, { all: true });
108+
if (Array.isArray(results)) {
109+
return results.map(result => result.address);
110+
}
111+
if (results && results.address) {
112+
return [results.address];
113+
}
114+
return [];
115+
} catch (error) {
116+
return null;
117+
}
118+
}
119+
120+
export function validateAttachmentUrl(urlString) {
121+
if (!urlString || typeof urlString !== 'string') {
122+
return { valid: false, reason: 'Empty URL' };
123+
}
124+
125+
let parsed;
126+
try {
127+
parsed = new URL(urlString);
128+
} catch (error) {
129+
return { valid: false, reason: 'Invalid URL format' };
130+
}
131+
132+
if (!['http:', 'https:'].includes(parsed.protocol)) {
133+
return { valid: false, reason: 'Only HTTP and HTTPS protocols are allowed' };
134+
}
135+
136+
const hostname = parsed.hostname;
137+
if (!hostname) {
138+
return { valid: false, reason: 'Missing hostname' };
139+
}
140+
141+
const lowerHostname = hostname.toLowerCase();
142+
if (BLOCKED_HOSTNAMES.has(lowerHostname) || lowerHostname.endsWith('.localhost')) {
143+
return { valid: false, reason: 'Localhost is not allowed' };
144+
}
145+
146+
if (!Meteor.isServer || !netModule) {
147+
return { valid: true };
148+
}
149+
150+
if (netModule.isIP(lowerHostname)) {
151+
return isIpBlocked(lowerHostname)
152+
? { valid: false, reason: 'IP address is not allowed' }
153+
: { valid: true };
154+
}
155+
156+
const addresses = resolveHostname(lowerHostname);
157+
if (!addresses || addresses.length === 0) {
158+
return { valid: false, reason: 'Hostname did not resolve' };
159+
}
160+
161+
const blockedAddress = addresses.find(address => isIpBlocked(address));
162+
if (blockedAddress) {
163+
return { valid: false, reason: 'Resolved IP address is not allowed' };
164+
}
165+
166+
return { valid: true };
167+
}

models/trelloCreator.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
calendar
2323
} from '/imports/lib/dateUtils';
2424
import getSlug from 'limax';
25+
import { validateAttachmentUrl } from './lib/attachmentUrlValidation';
2526

2627
const DateString = Match.Where(function(dateAsString) {
2728
check(dateAsString, String);
@@ -471,6 +472,17 @@ export class TrelloCreator {
471472
}
472473
};
473474
if (att.url) {
475+
const validation = validateAttachmentUrl(att.url);
476+
if (!validation.valid) {
477+
if (process.env.DEBUG === 'true') {
478+
console.warn(
479+
'Blocked attachment URL during Trello import:',
480+
validation.reason,
481+
att.url,
482+
);
483+
}
484+
return;
485+
}
474486
Attachments.load(att.url, opts, cb, true);
475487
} else if (att.file) {
476488
Attachments.insert(att.file, opts, cb, true);

models/wekanCreator.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
calendar
2222
} from '/imports/lib/dateUtils';
2323
import getSlug from 'limax';
24+
import { validateAttachmentUrl } from './lib/attachmentUrlValidation';
2425

2526
const DateString = Match.Where(function(dateAsString) {
2627
check(dateAsString, String);
@@ -519,6 +520,17 @@ export class WekanCreator {
519520
}
520521
};
521522
if (att.url) {
523+
const validation = validateAttachmentUrl(att.url);
524+
if (!validation.valid) {
525+
if (process.env.DEBUG === 'true') {
526+
console.warn(
527+
'Blocked attachment URL during Wekan import:',
528+
validation.reason,
529+
att.url,
530+
);
531+
}
532+
return;
533+
}
522534
Attachments.load(att.url, opts, cb, true);
523535
} else if (att.file) {
524536
Attachments.insert(att.file, opts, cb, true);

0 commit comments

Comments
 (0)