CVE-2023-22493 (published 2023-01-13T15:15Z)

RSSHub is an open source RSS feed generator. RSSHub is vulnerable to Server-Side Request Forgery (SSRF) attacks. This vulnerability allows an attacker to send arbitrary HTTP requests from the server to other servers or resources on the network. An attacker can exploit this vulnerability by sending a request to the affected routes with a malicious URL. An attacker could also use this vulnerability to send requests to internal or any other servers or resources on the network, potentially gain access to sensitive information that would not normally be accessible and amplifying the impact of the attack. The patch for this issue can be found in commit a66cbcf.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/DIYgod/RSSHub
commit_details: repo_url: https://github.com/DIYgod/RSSHub
author: Tony
author_date: 2023-01-10 11:45:05+00:00
msg: feat(config)!: unsafe domain toggle (#11588)
repository: repo_url: https://github.com/DIYgod/RSSHub
repo_name: DIYgod/RSSHub
description: 🧡 Everything is RSSible
repo_language: TypeScript
owner: DIYgod
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -564,7 +564,7 @@ It is also valid to contain route parameters, e.g. `/weibo/user/2612249974`.
 
 ::: tip Experimental features
 
-Configs in this sections are in beta stage, and are turn off by default. Please read corresponded description and turn on if necessary.
+Configs in this sections are in beta stage, and **are turn off by default**. Please read corresponded description and turn on if necessary.
 
 :::
 
@@ -572,6 +572,8 @@ Configs in this sections are in beta stage, and are turn off by default. Please
 
 `FILTER_REGEX_ENGINE`: Define Regex engine used in [Parameters->filtering](/en/parameter.html#filtering). Valid value are `[re2, regexp]`. Default value is `re2`. We suggest public instance should leave this value to default, and this option right now is mainly for backward compatibility.
 
+`ALLOW_USER_SUPPLY_UNSAFE_DOMAIN`: allow users to provide a domain as a parameter to routes that are not in their allow list, respectively. Public instances are suggested to leave this value default, as it may lead to [Server-Side Request Forgery (SSRF)](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
+
 ### Other Application Configurations
 
 `DISALLOW_ROBOT`: prevent indexing by search engine, default to enable, set false or 0 to disable

programming_language: Markdown
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -571,7 +571,7 @@ RSSHub 支持使用访问密钥 / 码，白名单和黑名单三种方式进行
 
 ::: tip 测试特性
 
-这个板块控制的是一些新特性的选项，默认他们都是关闭的。如果有需要请阅读对应说明后按需开启
+这个板块控制的是一些新特性的选项，他们都是**默认关闭**的。如果有需要请阅读对应说明后按需开启
 
 :::
 
@@ -579,6 +579,8 @@ RSSHub 支持使用访问密钥 / 码，白名单和黑名单三种方式进行
 
 `FILTER_REGEX_ENGINE`: 控制 [通用参数 -> 内容过滤](/parameter.html#nei-rong-guo-lu) 使用的正则引擎。可选`[re2, regexp]`，默认`re2`。我们推荐公开实例不要调整这个选项，这个选项目前主要用于向后兼容。
 
+`ALLOW_USER_SUPPLY_UNSAFE_DOMAIN`: 允许用户为路由提供域名作为参数。建议公共实例不要调整此选项，开启后可能会导致 [服务端请求伪造（SSRF）](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
+
 ### 其他应用配置
 
 `DISALLOW_ROBOT`: 阻止搜索引擎收录，默认开启，设置 false 或 0 关闭

programming_language: Markdown
filename: config.js
change_type: ModificationType.MODIFY
diff: @@ -97,6 +97,7 @@ const calculateValue = () => {
         feature: {
             allow_user_hotlink_template: envs.ALLOW_USER_HOTLINK_TEMPLATE === 'true',
             filter_regex_engine: envs.FILTER_REGEX_ENGINE || 're2',
+            allow_user_supply_unsafe_domain: envs.ALLOW_USER_SUPPLY_UNSAFE_DOMAIN === 'true',
         },
         suffix: envs.SUFFIX,
         titleLengthLimit: parseInt(envs.TITLE_LENGTH_LIMIT) || 150,

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const lang = ctx.params.lang || 'en';
     const id = ctx.params.id || 'bandizip';
+    if (!isValidHost(lang)) {
+        throw Error('Invalid language code');
+    }
 
     const rootUrl = `https://${lang}.bandisoft.com`;
     const currentUrl = `${rootUrl}/${id}/history/`;

programming_language: TypeScript
filename: others.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,12 @@
 const cheerio = require('cheerio');
 const got = require('@/utils/got');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
+    if (!isValidHost(ctx.params.column)) {
+        throw Error('Invalid column');
+    }
+
     const url = `http://${ctx.params.column}.bio1000.com/${ctx.params.id}`;
     const res = await got.get(url);
     const $ = cheerio.load(res.data);

programming_language: JavaScript
filename: hedwig.js
change_type: ModificationType.MODIFY
diff: @@ -4,9 +4,13 @@ const md = require('markdown-it')({
     html: true,
 });
 const dayjs = require('dayjs');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const type = ctx.params.type;
+    if (!isValidHost(type)) {
+        throw Error('Invalid type');
+    }
 
     const url = `https://${type}.hedwig.pub`;
     const res = await got({

programming_language: JavaScript
filename: wordpress.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,12 @@
 const parser = require('@/utils/rss-parser');
 const config = require('@/config').value;
+const allowDomain = ['lawrence.code.blog'];
 
 module.exports = async (ctx) => {
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowDomain.includes(ctx.params.domain)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
+
     const scheme = ctx.params.https || 'https';
     const cdn = config.wordpress.cdnUrl;
 

programming_language: JavaScript
filename: shop.js
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,13 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
-
+const { isValidHost } = require('@/utils/valid-host');
 const maxPages = 5;
 
 module.exports = async (ctx) => {
     const { subdomain } = ctx.params;
+    if (!isValidHost(subdomain)) {
+        throw Error('Invalid subdomain');
+    }
     const shopUrl = `https://${subdomain}.booth.pm`;
 
     let shopName;

programming_language: TypeScript
filename: blog.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 const got = require('@/utils/got');
 const parser = require('@/utils/rss-parser');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 async function load(link, need_feed_description) {
     const response = await got.get(link);
@@ -29,6 +30,9 @@ async function load(link, need_feed_description) {
 
 module.exports = async (ctx) => {
     const { column } = ctx.params;
+    if (!isValidHost(column)) {
+        throw Error('Invalid column');
+    }
     const link = `http://${column}.blog.caixin.com`;
     const feed_url = `${link}/feed`;
     const feed = await parser.parseURL(feed_url);

programming_language: TypeScript
filename: category.js
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,14 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const category = ctx.params.category;
     const column = ctx.params.column;
     const url = `http://${column}.caixin.com/${category}`;
+    if (!isValidHost(column)) {
+        throw Error('Invalid column');
+    }
 
     const response = await got({
         method: 'get',

programming_language: JavaScript
filename: search.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,12 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
+    if (!isValidHost(ctx.params.location)) {
+        throw Error('Invalid location');
+    }
+
     const queryParams = ctx.request.querystring;
     const queryUrl = `https://${ctx.params.location}.craigslist.org/search/${ctx.params.type}?${queryParams}`;
     const { data } = await got.get(queryUrl);

programming_language: TypeScript
filename: home.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const parser = require('@/utils/rss-parser');
+const allowLang = ['chinese', 'cn', 'us', 'japanese', 'www'];
 
 module.exports = async (ctx) => {
     const lang = ctx.params.lang === 'us' ? 'www' : ctx.params.lang || 'cn';
+    if (!allowLang.includes(lang)) {
+        throw Error('Invalid lang');
+    }
     const rssUrl = `https://${lang}.engadget.com/rss.xml`;
     const feed = await parser.parseURL(rssUrl);
 

programming_language: JavaScript
filename: main.js
change_type: ModificationType.MODIFY
diff: @@ -4,12 +4,15 @@
 // user?: fanbox domain name
 
 const got = require('@/utils/got');
-
+const { isValidHost } = require('@/utils/valid-host');
 const conv_item = require('./conv');
 const get_header = require('./header');
 
 module.exports = async (ctx) => {
     const user = ctx.params.user || 'official'; // if no user specified, just go to official page
+    if (!isValidHost(user)) {
+        throw Error('Invalid user');
+    }
     const box_url = `https://${user}.fanbox.cc`;
 
     // get user info

programming_language: TypeScript
filename: headline.js
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,12 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const country = ctx.params.country || 'ww';
+    if (!isValidHost(country)) {
+        throw Error('Invalid country');
+    }
 
     const rootUrl = `https://${country}.fashionnetwork.com`;
     const response = await got({

programming_language: TypeScript
filename: news.js
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,6 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const country = ctx.params.country || 'ww';
@@ -12,6 +13,10 @@ module.exports = async (ctx) => {
     const sectorsUrl = sectors ? 'sectors%5B%5D=' + sectors.split(',').join('&sectors%5B%5D=') : '';
     const categoriesUrl = categories ? 'categs%5B%5D=' + categories.split(',').join('&categs%5B%5D=') : '';
 
+    if (!isValidHost(country)) {
+        throw Error('Invalid country');
+    }
+
     const rootUrl = `https://${country}.fashionnetwork.com`;
     const currentUrl = `${rootUrl}/news/s.jsonp?${sectorsUrl}&${categoriesUrl}`;
     const response = await got({

programming_language: JavaScript
filename: common.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+const allowHost = ['gitlab.com'];
+
+module.exports = {
+    allowHost,
+};

programming_language: TypeScript
filename: explore.js
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,7 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const config = require('@/config').value;
+const { allowHost } = require('./common');
 
 module.exports = async (ctx) => {
     let { type, host } = ctx.params;
@@ -10,6 +12,9 @@ module.exports = async (ctx) => {
         starred: 'Most stars',
         all: 'All',
     };
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowHost.includes(new URL(host).hostname)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const res = await got({
         method: 'get',

programming_language: JavaScript
filename: release.js
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,13 @@
 const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
+const config = require('@/config').value;
+const { allowHost } = require('./common');
 
 module.exports = async (ctx) => {
     const { namespace, project, host } = ctx.params;
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowHost.includes(host)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const host_ = host ? host : 'gitlab.com';
     const namespace_ = encodeURIComponent(namespace);

programming_language: JavaScript
filename: tag.js
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,13 @@
 const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
+const config = require('@/config').value;
+const { allowHost } = require('./common');
 
 module.exports = async (ctx) => {
     const { namespace, project, host } = ctx.params;
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowHost.includes(host)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const host_ = host ? host : 'gitlab.com';
     const namespace_ = encodeURIComponent(namespace);

programming_language: TypeScript
filename: fluid.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,11 @@
 const cheerio = require('cheerio');
 const got = require('@/utils/got');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
+    if (!config.feature.allow_user_supply_unsafe_domain) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
     const url = `http://${ctx.params.url}`;
     const res = await got.get(`${url}/archives/`);
     const $ = cheerio.load(res.data);

programming_language: TypeScript
filename: next.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,11 @@
 const cheerio = require('cheerio');
 const got = require('@/utils/got');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
+    if (!config.feature.allow_user_supply_unsafe_domain) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
     const url = `http://${ctx.params.url}`;
     const res = await got.get(`${url}/archives/`);
     const $ = cheerio.load(res.data);

programming_language: JavaScript
filename: yilia.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,11 @@
 const cheerio = require('cheerio');
 const got = require('@/utils/got');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
+    if (!config.feature.allow_user_supply_unsafe_domain) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
     const url = `http://${ctx.params.url}`;
     const res = await got.get(url);
     const $ = cheerio.load(res.data);

programming_language: TypeScript
filename: account_id.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const utils = require('./utils');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
     const site = ctx.params.site;
     const account_id = ctx.params.account_id;
     const only_media = ctx.params.only_media ? 'true' : 'false';
+    if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const { account_data, data } = await utils.getAccountStatuses(site, account_id, only_media);
 

programming_language: JavaScript
filename: timeline_local.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const utils = require('./utils');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
     const site = ctx.params.site;
     const only_media = ctx.params.only_media ? 'true' : 'false';
+    if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const url = `http://${site}/api/v1/timelines/public?local=true&only_media=${only_media}`;
 

programming_language: JavaScript
filename: timeline_remote.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const utils = require('./utils');
+const config = require('@/config').value;
 
 module.exports = async (ctx) => {
     const site = ctx.params.site;
     const only_media = ctx.params.only_media ? 'true' : 'false';
+    if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const url = `http://${site}/api/v1/timelines/public?remote=true&only_media=${only_media}`;
 

programming_language: JavaScript
filename: utils.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
 
+const allowSiteList = ['mastodon.social', 'pawoo.net'];
+
 const parseStatuses = (data) =>
     data.map((item) => {
         // docs on: https://docs.joinmastodon.org/entities/status/
@@ -125,4 +127,5 @@ module.exports = {
     parseStatuses,
     getAccountStatuses,
     getAccountIdByAcct,
+    allowSiteList,
 };

programming_language: JavaScript
filename: category_url.js
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,14 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const language = ctx.params.language || 'www';
     const url = ctx.params.url || 'video';
     const link = `https://${language}.pornhub.com/${url}`;
+    if (!isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const response = await got.get(link);
     const $ = cheerio.load(response.data);

programming_language: TypeScript
filename: model.js
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,15 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const language = ctx.params.language || 'www';
     const username = ctx.params.username;
     const sort = ctx.params.sort || 'mr';
     const link = `https://${language}.pornhub.com/model/${username}/videos?o=${sort}`;
+    if (!isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const response = await got.get(link);
     const $ = cheerio.load(response.data);

programming_language: TypeScript
filename: pornstar.js
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,15 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const language = ctx.params.language || 'www';
     const username = ctx.params.username;
     const sort = ctx.params.sort || 'mr';
     const link = `https://${language}.pornhub.com/pornstar/${username}/videos?o=${sort}`;
+    if (!isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const response = await got.get(link);
     const $ = cheerio.load(response.data);

programming_language: TypeScript
filename: users.js
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,14 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const language = ctx.params.language || 'www';
     const username = ctx.params.username;
     const link = `https://${language}.pornhub.com/users/${username}/videos`;
+    if (!isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const response = await got.get(link);
     const $ = cheerio.load(response.data);

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,9 @@ const getBaseUrl = (language) => (language === 'ja' ? 'https://touhougarakuta.co
 
 module.exports = async (ctx) => {
     const { language, type } = ctx.params;
+    if (!Object.keys(languageCodes).includes(language)) {
+        throw Error('Invalid language');
+    }
 
     const baseUrl = getBaseUrl(language);
 

programming_language: TypeScript
filename: report.js
change_type: ModificationType.MODIFY
diff: @@ -1,13 +1,17 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
-    ctx.params.lang = ctx.params.lang || 'www';
-    ctx.params.platform = ctx.params.platform || '';
-    ctx.params.year = ctx.params.year || '';
-
-    const rootUrl = `https://${ctx.params.lang === 'en' ? 'www' : ctx.params.lang}.weforum.org`;
-    const currentUrl = `${rootUrl}/reports?platform=${ctx.params.platform}&year=${ctx.params.year}`;
+    const lang = ctx.params.lang || 'www';
+    const platform = ctx.params.platform || '';
+    const year = ctx.params.year || '';
+    if (!isValidHost(lang)) {
+        throw Error('Invalid lang');
+    }
+
+    const rootUrl = `https://${lang === 'en' ? 'www' : lang}.weforum.org`;
+    const currentUrl = `${rootUrl}/reports?platform=${platform}&year=${year}`;
     const response = await got({
         method: 'get',
         url: currentUrl,

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const parser = require('@/utils/rss-parser');
 const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const region = ctx.params.region === 'en' ? '' : ctx.params.region.toLowerCase() + '.';
+    if (!isValidHost(region)) {
+        throw Error('Invalid region');
+    }
     const category = ctx.params.category ? ctx.params.category.toLowerCase() : '';
     const rssUrl = `https://${region}news.yahoo.com/rss/${category}`;
     const feed = await parser.parseURL(rssUrl);

programming_language: JavaScript
filename: room.js
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,5 @@
 const got = require('@/utils/got');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const city = ctx.params.city || 'sh';
@@ -7,6 +8,10 @@ module.exports = async (ctx) => {
     const room = ctx.params.room || '1';
     const domain = `${city === 'bj' ? '' : city + '.'}m.ziroom.com`;
 
+    if (!isValidHost(city)) {
+        throw Error('Invalid city');
+    }
+
     const response = await got({
         method: 'post',
         url: `http://${domain}/list/ajax-get-data`,

programming_language: JavaScript
filename: valid-host.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,16 @@
+/**
+ * Check if a sub-domain is valid
+ * @param {String} hostname sub-domain
+ * @returns {Boolean} true if valid
+ */
+const isValidHost = (hostname) => {
+    if (typeof hostname !== 'string') {
+        return false;
+    }
+    const regex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
+    return regex.test(hostname);
+};
+
+module.exports = {
+    isValidHost,
+};

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,7 @@ const cheerio = require('cheerio');
 const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
 const iconv = require('iconv-lite');
+const { isValidHost } = require('@/utils/valid-host');
 
 const setCookie = function (cookieName, cookieValue, seconds, path, domain, secure) {
     let expires = null;
@@ -15,6 +16,9 @@ const setCookie = function (cookieName, cookieValue, seconds, path, domain, secu
 
 module.exports = async (ctx) => {
     const city = ctx.params.city ?? 'www';
+    if (!isValidHost(city)) {
+        throw Error('Invalid city');
+    }
 
     const rootUrl = `https://${city}.19lou.com`;
 

programming_language: TypeScript
filename: author.js
change_type: ModificationType.MODIFY
diff: @@ -3,11 +3,13 @@ const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { domainValidation } = require('./utils');
 
 module.exports = async (ctx) => {
     const { domain = '91porn.com' } = ctx.query;
     const { uid, lang = 'en_US' } = ctx.params;
     const siteUrl = `https://${domain}/uvideos.php?UID=${uid}&type=public`;
+    domainValidation(domain, ctx);
 
     const response = await got.post(siteUrl, {
         form: {

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,11 +3,13 @@ const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { domainValidation } = require('./utils');
 
 module.exports = async (ctx) => {
     const { domain = '91porn.com' } = ctx.query;
     const siteUrl = `https://${domain}/index.php`;
     const { lang = 'en_US' } = ctx.params;
+    domainValidation(domain, ctx);
 
     const response = await got.post(siteUrl, {
         form: {

programming_language: TypeScript
filename: utils.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,12 @@
+const config = require('@/config').value;
+const allowDomain = ['91porn.com', 'www.91porn.com', '0122.91p30.com', 'www.91zuixindizhi.com', 'w1218.91p46.com'];
+
+const domainValidation = (domain, ctx) => {
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowDomain.includes(domain)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
+};
+
+module.exports = {
+    domainValidation,
+};

programming_language: TypeScript
filename: news.js
change_type: ModificationType.MODIFY
diff: @@ -2,9 +2,13 @@ const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const city = ctx.params.city;
+    if (!isValidHost(city)) {
+        throw Error('Invalid city');
+    }
 
     const rootUrl = `http://${city}.bendibao.com`;
 

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,10 +3,31 @@ const cheerio = require('cheerio');
 const iconv = require('iconv-lite');
 const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
+const config = require('@/config').value;
+const allowHost = [
+    'www.xbiquwx.la',
+    'www.biqu5200.net',
+    'www.xbiquge.so',
+    'www.biqugeu.net',
+    'www.b520.cc',
+    'www.ahfgb.com',
+    'www.ibiquge.la',
+    'www.biquge.tv',
+    'www.bswtan.com',
+    'www.biquge.co',
+    'www.bqzhh.com',
+    'www.biqugse.com',
+    'www.ibiquge.info',
+    'www.ishuquge.com',
+    'www.mayiwxw.com',
+];
 
 module.exports = async (ctx) => {
     const rootUrl = ctx.path.split('/').slice(1, 4).join('/');
     const currentUrl = ctx.path.slice(1);
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowHost.includes(new URL(rootUrl).hostname)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     const response = await got({
         method: 'get',

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -4,10 +4,15 @@ const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const config = require('@/config').value;
+const allowDomain = ['btbtt15.com'];
 
 module.exports = async (ctx) => {
     let category = ctx.params.category ?? '';
-    let domain = ctx.query.domain ?? 'btbtt20.com';
+    let domain = ctx.query.domain ?? 'btbtt15.com';
+    if (!config.feature.allow_user_supply_unsafe_domain && !allowDomain.includes(new URL(domain).hostname)) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
     if (category === 'base') {
         category = '';

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -12,6 +12,9 @@ const categories = {
 module.exports = async (ctx) => {
     const category = ctx.params.category ?? 'jxrb';
     const id = ctx.params.id;
+    if (!Object.keys(categories).includes(category)) {
+        throw Error('Invalid category');
+    }
 
     const rootUrl = `https://${category}.cnjxol.com`;
     const currentUrl = `${rootUrl}/${category}Paper/pc/layout`;

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,9 +3,13 @@ const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const defaults = require('./defaults');
 const shortcuts = require('./shortcuts');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const site = ctx.params[0] ?? 'news';
+    if (!isValidHost(site)) {
+        throw Error('Invalid site');
+    }
 
     let items;
     let category = ctx.params[1] ?? (defaults.hasOwnProperty(site) ? defaults[site] : '');

programming_language: JavaScript
filename: blog.js
change_type: ModificationType.MODIFY
diff: @@ -1,16 +1,20 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
-
+const { isValidHost } = require('@/utils/valid-host');
 const cateList = ['all', 'design-resources', 'learn-design', 'inside-eagle'];
 
 module.exports = async (ctx) => {
     let cate = ctx.params.cate ?? 'all';
     let language = ctx.params.language ?? 'cn';
-    if (cateList.indexOf(cate) === -1) {
+    if (!isValidHost(cate) || !isValidHost(language)) {
+        throw Error('Invalid host');
+    }
+    if (!cateList.includes(cate)) {
         language = cate;
         cate = 'all';
     }
+
     const host = `https://${language}.eagle.cool`;
     const url = `${host}/blog/${cate === 'all' ? '' : cate}`;
 

programming_language: TypeScript
filename: rss.js
change_type: ModificationType.MODIFY
diff: @@ -4,9 +4,13 @@ const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const allowRegion = ['tw', 'hk'];
 
 module.exports = async (ctx) => {
     const region = ctx.params.region ?? 'tw';
+    if (!allowRegion.includes(region)) {
+        throw Error('Invalid region');
+    }
 
     const feed = await parser.parseURL(`https://www.eprice.com.${region}/news/rss.xml`);
 

programming_language: JavaScript
filename: ff14_global.js
change_type: ModificationType.MODIFY
diff: @@ -2,11 +2,16 @@ const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const lang = ctx.params.lang;
     const type = ctx.params.type ?? 'all';
 
+    if (!isValidHost(lang)) {
+        throw Error('Invalid lang');
+    }
+
     const response = await got({
         method: 'get',
         url: `https://lodestonenews.com/news/${type}?locale=${lang}`,

programming_language: TypeScript
filename: category.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const parser = require('@/utils/rss-parser');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const { domain = 'news', category } = ctx.params;
+    if (!isValidHost(domain)) {
+        throw Error('Invalid domain');
+    }
     const baseUrl = `https://${domain}.gamme.com.tw`;
     const feed = await parser.parseURL(`${baseUrl + (category ? `/category/${category}` : '')}/feed`);
 

programming_language: JavaScript
filename: tag.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const { domain = 'news', tag } = ctx.params;
+    if (!isValidHost(domain)) {
+        throw Error('Invalid domain');
+    }
     const baseUrl = `https://${domain}.gamme.com.tw`;
     const pageUrl = `${baseUrl}/tag/${tag}`;
 

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -2,10 +2,14 @@ const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const username = ctx.params.username;
     const products = ctx.params.products;
+    if (!isValidHost(username)) {
+        throw Error('Invalid username');
+    }
     const url = `https://${username}.gumroad.com/l/${products}`;
 
     const response = await got(url);

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 function getKeysRecursive(dic, key, attr, array) {
     Object.values(dic).forEach((v) => {
@@ -15,6 +16,10 @@ function getKeysRecursive(dic, key, attr, array) {
 
 module.exports = async (ctx) => {
     const category = ctx.params.category ?? 'china';
+    if (!isValidHost(category)) {
+        throw Error('Invalid category');
+    }
+
     const host = 'https://' + category + '.huanqiu.com';
 
     const resp = await got({

programming_language: JavaScript
filename: devlog.js
change_type: ModificationType.MODIFY
diff: @@ -4,10 +4,14 @@ const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const user = ctx.params.user ?? '';
     const id = ctx.params.id ?? '';
+    if (!isValidHost(user)) {
+        throw Error('Invalid user');
+    }
 
     const rootUrl = `https://${user}.itch.io/${id}/devlog`;
 

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,17 +3,27 @@ const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const config = require('@/config').value;
 
 const toSize = (raw) => {
     const matches = raw.match(/(\d+(\.\d+)?)(\w+)/);
     return matches[3] === 'GB' ? matches[1] * 1024 : matches[1];
 };
 
+const allowDomain = ['javbus.com', 'javbus.org', 'javsee.icu', 'javsee.one'];
+
 module.exports = async (ctx) => {
     const isWestern = /^\/western/.test(ctx.path);
+    const domain = ctx.query.domain ?? 'javbus.com';
+    const westernDomain = ctx.query.western_domain ?? 'javbus.org';
+
+    const rootUrl = `https://www.${domain}`;
+    const westernUrl = `https://www.${westernDomain}`;
+
+    if (!config.feature.allow_user_supply_unsafe_domain && (!allowDomain.includes(new URL(domain).hostname) || !allowDomain.includes(new URL(westernDomain).hostname))) {
+        ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+    }
 
-    const rootUrl = `https://www.${ctx.query.domain ?? 'javbus.com'}`;
-    const westernUrl = `https://www.${ctx.query.western_domain ?? 'javbus.org'}`;
     const currentUrl = `${isWestern ? westernUrl : rootUrl}${ctx.path.replace(/^\/western/, '').replace(/\/home/, '')}`;
 
     const response = await got({

programming_language: TypeScript
filename: utils.js
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,16 @@
 const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
+const config = require('@/config').value;
+const allowDomain = ['javdb.com', 'javdb36.com', 'javdb007.com'];
 
 module.exports = {
     ProcessItems: async (ctx, currentUrl, title) => {
         const domain = ctx.query.domain ?? 'javdb.com';
+        if (!config.feature.allow_user_supply_unsafe_domain && !allowDomain.includes(new URL(domain).hostname)) {
+            ctx.throw(403, `This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
+        }
+
         const rootUrl = `https://${domain}`;
 
         const response = await got({

programming_language: JavaScript
filename: user.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const name = ctx.params.name ?? 'i';
     const limit = ctx.query.limit ? parseInt(ctx.query.limit) : '50';
+    if (!isValidHost(name)) {
+        throw Error('Invalid name');
+    }
 
     const rootUrl = `${name}.lofter.com`;
 

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -4,10 +4,13 @@ const md = require('markdown-it')({
     html: true,
     linkify: true,
 });
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const id = ctx.params.id;
-
+    if (!id.endsWith('.eth') && !isValidHost(id)) {
+        throw Error('Invalid id');
+    }
     const rootUrl = 'https://mirror.xyz';
     const currentUrl = id.endsWith('.eth') ? `${rootUrl}/${id}` : `https://${id}.mirror.xyz`;
 

programming_language: JavaScript
filename: activity.js
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,7 @@ const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const category = ctx.params.category ?? '-1';
@@ -13,6 +14,10 @@ module.exports = async (ctx) => {
     const latestAlerts = ctx.params.latestAlerts ?? '1';
     const latestPictures = ctx.params.latestPictures ?? '1';
 
+    if (language && !isValidHost(language)) {
+        throw Error('Invalid language');
+    }
+
     const rootUrl = `https://${language === 'en' || language === '' ? '' : `${language}.`}myfigurecollection.net`;
     const currentUrl = `${rootUrl}/browse.v4.php?mode=activity&latestAdditions=${latestAdditions}&latestEdits=${latestEdits}&latestAlerts=${latestAlerts}&latestPictures=${latestPictures}&rootId=${category}`;
 

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { art } = require('@/utils/render');
 const path = require('path');
+const { isValidHost } = require('@/utils/valid-host');
 
 const shortcuts = {
     potd: 'picture/browse/potd/',
@@ -12,6 +13,9 @@ const shortcuts = {
 module.exports = async (ctx) => {
     const language = ctx.params.language ?? '';
     const category = ctx.params.category ?? 'figure';
+    if (language && !isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const rootUrl = `https://${language === 'en' || language === '' ? '' : `${language}.`}myfigurecollection.net`;
     const currentUrl = `${rootUrl}/${shortcuts.hasOwnProperty(category) ? shortcuts[category] : category}`;

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 const cleanContent = (language, content) => {
     switch (language) {
@@ -23,6 +24,9 @@ module.exports = async (ctx) => {
     const language = ctx.params.language ?? 'cn';
     const category = ctx.params.category ?? '';
     const type = ctx.params.type ?? '';
+    if (!isValidHost(language)) {
+        throw Error('Invalid language');
+    }
 
     const rootUrl = `https://${language === 'zh' ? `zh.cn` : language}.nikkei.com`;
     const currentUrl = `${rootUrl}/${category ? (category === 'rss' ? 'rss.html' : `${category}${type ? `/${type}` : ''}.html`) : ''}`;

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -3,12 +3,16 @@ const cheerio = require('cheerio');
 const iconv = require('iconv-lite');
 const timezone = require('@/utils/timezone');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const site = ctx.params[0] ?? 'www';
     let category = ctx.params[1] ?? (site === 'www' ? '59476' : '');
     category = site === 'cpc' && category === '24h' ? '87228' : category;
 
+    if (!isValidHost(site)) {
+        throw Error('Invalid site');
+    }
     const rootUrl = `http://${site}.people.com.cn`;
     const currentUrl = `${rootUrl}/GB/${category}/index.html`;
 

programming_language: JavaScript
filename: journal.js
change_type: ModificationType.MODIFY
diff: @@ -1,12 +1,16 @@
 const cheerio = require('cheerio');
 const { puppeteerGet, renderDesc } = require('./utils');
 const config = require('@/config').value;
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const pub = ctx.params.pub;
     const jrn = ctx.params.jrn;
     const host = `https://${pub}.scitation.org`;
     const jrnlUrl = `${host}/toc/${jrn}/current?size=all`;
+    if (!isValidHost(pub)) {
+        throw Error('Invalid pub');
+    }
 
     // use Puppeteer due to the obstacle by cloudflare challenge
     const browser = await require('@/utils/puppeteer')();

programming_language: TypeScript
filename: section.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 const cheerio = require('cheerio');
 const { puppeteerGet, renderDesc } = require('./utils');
 const config = require('@/config').value;
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const pub = ctx.params.pub;
@@ -8,6 +9,9 @@ module.exports = async (ctx) => {
     const sec = ctx.params.sec.split('+').join(' ');
     const host = `https://${pub}.scitation.org`;
     const jrnlUrl = `${host}/toc/${jrn}/current?size=all`;
+    if (!isValidHost(pub)) {
+        throw Error('Invalid pub');
+    }
 
     // use Puppeteer due to the obstacle by cloudflare challenge
     const browser = await require('@/utils/puppeteer')();

programming_language: TypeScript
filename: main.js
change_type: ModificationType.MODIFY
diff: @@ -6,9 +6,14 @@
 const got = require('@/utils/got'); // get web content
 const cheerio = require('cheerio'); // html parser
 const get_article = require('./_article');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const type = ctx.params.type ?? 'www';
+    if (!isValidHost(type)) {
+        throw Error('Invalid type');
+    }
+
     const base_url = `https://${type}.solidot.org`;
     const response = await got({
         method: 'get',

programming_language: TypeScript
filename: user.js
change_type: ModificationType.MODIFY
diff: @@ -2,11 +2,15 @@ const got = require('@/utils/got');
 const cheerio = require('cheerio');
 const { parseDate } = require('@/utils/parse-date');
 const { extractArticle, extractWork } = require('./utils');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const { uid } = ctx.params;
     let pageUrl = `https://www.zcool.com.cn/u/${uid}`;
     if (isNaN(uid)) {
+        if (!isValidHost(uid)) {
+            throw Error('Invalid uid');
+        }
         pageUrl = `https://${uid}.zcool.com.cn`;
     }
     const { data: response } = await got(pageUrl);

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,13 @@
 const got = require('@/utils/got');
 const { parseDate } = require('@/utils/parse-date');
+const { isValidHost } = require('@/utils/valid-host');
 
 module.exports = async (ctx) => {
     const { id } = ctx.params;
     const limit = ctx.query.limit ? parseInt(ctx.query.limit) : 20;
+    if (!isValidHost(id)) {
+        throw Error('Invalid id');
+    }
 
     const response = await got({
         method: 'get',

programming_language: JavaScript
filename: valid-host.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,21 @@
+const { isValidHost } = require('../../lib/utils/valid-host');
+
+describe('valid-host', () => {
+    it('validate hostname', () => {
+        expect(isValidHost()).toBe(false);
+        expect(isValidHost(123)).toBe(false);
+        expect(isValidHost('')).toBe(false);
+        expect(isValidHost('subd0main')).toBe(true);
+        expect(isValidHost('-subd0main')).toBe(false);
+        expect(isValidHost('sub-d0main')).toBe(true);
+        expect(isValidHost('subd0main-')).toBe(false);
+        expect(isValidHost('sub.d0main')).toBe(false);
+        expect(isValidHost('sub-.d0main')).toBe(false);
+        expect(isValidHost('s')).toBe(true);
+        expect(isValidHost('-')).toBe(false);
+        expect(isValidHost('0')).toBe(true);
+        expect(isValidHost('s-')).toBe(false);
+        expect(isValidHost('s-u')).toBe(true);
+        expect(isValidHost('su')).toBe(true);
+    });
+});

programming_language: TypeScript