Skip to content

Commit e2429e2

Browse files
authored
newsfeed: update checkArticleUrl (#4256)
1 parent 8a9c4a4 commit e2429e2

1 file changed

Lines changed: 41 additions & 3 deletions

File tree

‎defaultmodules/newsfeed/node_helper.js‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
const dns = require("node:dns");
2+
const ipaddr = require("ipaddr.js");
3+
const undici = require("undici");
14
const NodeHelper = require("node_helper");
25
const Log = require("logger");
36
const NewsfeedFetcher = require("./newsfeedfetcher");
@@ -22,18 +25,53 @@ module.exports = NodeHelper.create({
2225
* Checks whether a URL can be displayed in an iframe by inspecting
2326
* X-Frame-Options and Content-Security-Policy headers server-side.
2427
* @param {string} url The article URL to check.
28+
* @returns {null} sendSocketNotification
2529
*/
2630
async checkArticleUrl (url) {
2731
try {
28-
const response = await fetch(url, { method: "HEAD" });
32+
// 1. Parse URL
33+
let parsed;
34+
try {
35+
parsed = new URL(url);
36+
} catch {
37+
return this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: false });
38+
}
39+
40+
// 2. Protocol validation
41+
if (!["http:", "https:"].includes(parsed.protocol)) {
42+
return this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: false });
43+
}
44+
45+
// 3. Block localhost hostname
46+
if (parsed.hostname.toLowerCase() === "localhost") {
47+
return this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: false });
48+
}
49+
50+
// 4. DNS lookup + IP range validation
51+
const { address, family } = await dns.promises.lookup(parsed.hostname);
52+
if (ipaddr.process(address).range() !== "unicast") {
53+
Log.warn(`SSRF blocked in checkArticleUrl: ${url}`);
54+
return this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: false });
55+
}
56+
57+
// 5. Pin IP to prevent DNS rebinding
58+
const dispatcher = new undici.Agent({
59+
connect: {
60+
lookup: (_h, _o, cb) => {
61+
process.nextTick(() => cb(null, [{ address, family }]));
62+
}
63+
}
64+
});
65+
66+
// 6. Make request with pinned IP
67+
const response = await undici.fetch(url, { dispatcher, method: "HEAD" });
2968
const xfo = response.headers.get("x-frame-options");
3069
const csp = response.headers.get("content-security-policy");
31-
// sameorigin also blocks since the article is on a different origin than MM
3270
const blockedByXFO = xfo && ["deny", "sameorigin"].includes(xfo.toLowerCase().trim());
3371
const blockedByCSP = csp && (/frame-ancestors\s+['"]?none['"]?/).test(csp);
72+
3473
this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: !blockedByXFO && !blockedByCSP });
3574
} catch {
36-
// Network error or HEAD not supported — let the browser try the iframe anyway
3775
this.sendSocketNotification("ARTICLE_URL_STATUS", { url, canFrame: true });
3876
}
3977
},

0 commit comments

Comments
 (0)