CVE-2024-27926: Insufficient Information (NVD-CWE-noinfo)
value: RSSHub is an open source RSS feed generator. Starting in version 1.0.0-master.cbbd829 and prior to version 1.0.0-master.d8ca915, ahen the specially crafted image is supplied to the internal media proxy, it proxies the image without handling XSS vulnerabilities, allowing for the execution of arbitrary JavaScript code. Users who access the deliberately constructed URL are affected. This vulnerability was fixed in version 1.0.0-master.d8ca915. No known workarounds are available.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-21T02:52Z
Repository: https://github.com/DIYgod/RSSHub
Language: TypeScript
Files changed: lib/config.ts, lib/routes/rsshub/maintainer.ts, lib/routes/rsshub/router.ts, lib/routes/telegram/channel.ts, lib/routes/telegram/tglib/channel.ts, lib/routes/telegram/tglib/client.ts, lib/routes/weibo/timeline.ts, website/docs/install/config.md, website/docs/routes/other.mdx, website/docs/routes/social-media.mdx, website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md
Commit date: 2024-03-04 23:06:41 +0800
Commit message: feat: remove MEDIA_PROXY_KEY and internal media proxy

Diff: diff --git a/lib/config.ts b/lib/config.ts
index 5f5fa08f09..d02536fb29 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -59,11 +59,10 @@ export type Config = {
     };
     feature: {
         allow_user_hotlink_template: boolean;
         filter_regex_engine: string;
         allow_user_supply_unsafe_domain: boolean;
-        mediaProxyKey?: string;
     };
     suffix?: string;
     titleLengthLimit: number;
     openai: {
         apiKey?: string;
@@ -389,11 +388,10 @@ const calculateValue = () => {
         },
         feature: {
             allow_user_hotlink_template: toBoolean(envs.ALLOW_USER_HOTLINK_TEMPLATE, false),
             filter_regex_engine: envs.FILTER_REGEX_ENGINE || 're2',
             allow_user_supply_unsafe_domain: toBoolean(envs.ALLOW_USER_SUPPLY_UNSAFE_DOMAIN, false),
-            mediaProxyKey: envs.MEDIA_PROXY_KEY,
         },
         suffix: envs.SUFFIX,
         titleLengthLimit: toInt(envs.TITLE_LENGTH_LIMIT, 150),
         openai: {
             apiKey: envs.OPENAI_API_KEY,
diff --git a/lib/routes/rsshub/maintainer.ts b/lib/routes/rsshub/maintainer.ts
index c280129158..8bcc0e7e59 100644
--- a/lib/routes/rsshub/maintainer.ts
+++ b/lib/routes/rsshub/maintainer.ts
@@ -1,7 +1,6 @@
 export default {
-    '/m/:key/:url': ['TonyRL'],
     '/routes/:lang?': ['DIYgod'],
     '/transform/html/:url/:routeParams': ['ttttmr'],
     '/transform/json/:url/:routeParams': ['ttttmr'],
     '/transform/sitemap/:url/:routeParams?': ['flrngel'],
 };
diff --git a/lib/routes/rsshub/media.ts b/lib/routes/rsshub/media.ts
deleted file mode 100644
index e05dcb269f..0000000000
--- a/lib/routes/rsshub/media.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// @ts-nocheck
-import got from '@/utils/got';
-import { config } from '@/config';
-const { getDomain } = require('tldts');
-const { refererMap } = require('./referer-map');
-
-export default async (ctx) => {
-    if (!config.feature.mediaProxyKey) {
-        throw new Error('Internal media proxy is disabled.');
-    }
-
-    const key = ctx.req.param('key');
-    if (key !== config.feature.mediaProxyKey) {
-        throw new Error('Invalid media proxy key.');
-    }
-
-    const url = decodeURIComponent(ctx.req.param('url'));
-    const requestUrl = new URL(url);
-    const { hostname, origin } = requestUrl;
-
-    const domain = getDomain(hostname);
-
-    let referer = refererMap.get(domain);
-    referer ||= origin;
-
-    const { headers } = await got.head(url, {
-        headers: {
-            referer,
-        },
-    });
-
-    const cacheControl = headers['cache-control'];
-    const contentType = headers['content-type'];
-    const contentLength = headers['content-length'];
-
-    if (!contentType.startsWith('image/') || headers.server === 'RSSHub') {
-        return ctx.redirect(url);
-    }
-
-    ctx.set({
-        'cache-control': cacheControl || `public, max-age=${config.cache.contentExpire}`,
-        'content-length': contentLength,
-        'content-type': contentType,
-        server: 'RSSHub',
-    });
-
-    ctx.body = await got.stream(url, {
-        headers: {
-            referer,
-        },
-    });
-};
diff --git a/lib/routes/rsshub/router.ts b/lib/routes/rsshub/router.ts
index 1092016375..1f25613c30 100644
--- a/lib/routes/rsshub/router.ts
+++ b/lib/routes/rsshub/router.ts
@@ -1,7 +1,6 @@
 export default (router) => {
-    router.get('/m/:key/:url', './media');
     router.get('/routes/:lang?', './routes');
     router.get('/transform/html/:url/:routeParams', './transform/html');
     router.get('/transform/json/:url/:routeParams', './transform/json');
     router.get('/transform/sitemap/:url/:routeParams?', './transform/sitemap');
 };
diff --git a/lib/routes/telegram/channel.ts b/lib/routes/telegram/channel.ts
index 919c67c80b..1bab80671d 100644
--- a/lib/routes/telegram/channel.ts
+++ b/lib/routes/telegram/channel.ts
@@ -53,11 +53,11 @@ const mediaTagDict = {
     UNSUPPORTED: ['[Unsupported]', '🚫'],
     PARTIALLY_UNSUPPORTED: ['', ''],
 };
 
 export default async (ctx) => {
-    const useWeb = ctx.req.param('routeParams') || !(config.telegram.session && config.feature.mediaProxyKey);
+    const useWeb = ctx.req.param('routeParams') || !config.telegram.session;
     if (!useWeb) {
         return require('./tglib/channel').default(ctx);
     }
 
     const username = ctx.req.param('username');
diff --git a/lib/routes/telegram/tglib/channel.ts b/lib/routes/telegram/tglib/channel.ts
index b9d1431d5f..c16f1230cc 100644
--- a/lib/routes/telegram/tglib/channel.ts
+++ b/lib/routes/telegram/tglib/channel.ts
@@ -31,14 +31,10 @@ function parseRange(range, length) {
     }
     return parsedSegs;
 }
 
 async function getMedia(ctx) {
-    if (ctx.req.param('key') !== config.feature.mediaProxyKey) {
-        throw new Error('Invalid key');
-    }
-
     const media = await decodeMedia(ctx.req.param('username'), ctx.req.param('media'));
     if (!media) {
         ctx.status = 500;
         return ctx.res.end();
     }
diff --git a/lib/routes/telegram/tglib/client.ts b/lib/routes/telegram/tglib/client.ts
index a92254e79f..a99814fd78 100644
--- a/lib/routes/telegram/tglib/client.ts
+++ b/lib/routes/telegram/tglib/client.ts
@@ -67,11 +67,11 @@ function ExpandInlineBytes(bytes) {
     footer.copy(real, header.length + bytes.length, 0);
     return real;
 }
 
 function getMediaLink(ctx, channel, channelName, message) {
-    const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}/${config.feature.mediaProxyKey}/`;
+    const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}`;
     const src = base + `${channel.channelId}_${message.id}`;
 
     const x = message.media;
     if (x instanceof Api.MessageMediaPhoto || (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('image/'))) {
         return `<img src="${src}" alt=""/>`;
diff --git a/lib/routes/weibo/timeline.ts b/lib/routes/weibo/timeline.ts
index 1ad612e207..c9377982fa 100644
--- a/lib/routes/weibo/timeline.ts
+++ b/lib/routes/weibo/timeline.ts
@@ -143,11 +143,11 @@ export default async (ctx) => {
 
             ctx.set({
                 'Content-Type': 'text/html; charset=UTF-8',
                 'Cache-Control': 'no-cache',
             });
-            ctx.body = `<script>window.location = '/weibo/timeline/${uid}${routeParams ? `/${routeParams}` : ''}'</script>`;
+            ctx.html(`<script>window.location = '/weibo/timeline/${uid}${routeParams ? `/${routeParams}` : ''}'</script>`);
         }
     } else {
         const { app_key = '', redirect_url = ctx.req.origin + '/weibo/timeline/0' } = config.weibo;
 
         ctx.status = 302;
diff --git a/website/docs/install/config.md b/website/docs/install/config.md
index 84c52f2a65..d041eb80b6 100644
--- a/website/docs/install/config.md
+++ b/website/docs/install/config.md
@@ -196,12 +196,10 @@ Configs in this sections are in beta stage, and **are turn off by default**. Ple
 
 `FILTER_REGEX_ENGINE`: Define Regex engine used in [Parameters->filtering](/parameter#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)
 
-`MEDIA_PROXY_KEY`: the access key for internal media proxy.
-
 ## Other Application Configurations
 
 `DISALLOW_ROBOT`: prevent indexing by search engine, default to enable, set false or 0 to disable
 
 `ENABLE_CLUSTER`: enable cluster mode, default to `false`
diff --git a/website/docs/routes/other.mdx b/website/docs/routes/other.mdx
index cb9d1607fc..a594dc3758 100644
--- a/website/docs/routes/other.mdx
+++ b/website/docs/routes/other.mdx
@@ -119,11 +119,11 @@ See [#app-store-mac-app-store](/routes/program-update#app-store-mac-app-store)
 
 ### South China Morning Post - China coronavirus outbreak {#corona-virus-disease-2019-south-china-morning-post-china-coronavirus-outbreak}
 
 <Route author="DIYgod" example="/scmp/coronavirus" path="/scmp/coronavirus" />
 
-### Macao Pagina Electrónica Especial Contra Epidemias: What’s New {#corona-virus-disease-2019-macao-pagina-electr%C3%B3nica-especial-contra-epidemias-what-s-new}
+### Macao Pagina Electrónica Especial Contra Epidemias: What’s New {#corona-virus-disease-2019-macao-pagina-electronica-especial-contra-epidemias-what-s-new}
 
 Official Website: [https://www.ssm.gov.mo/apps1/PreventWuhanInfection/en.aspx](https://www.ssm.gov.mo/apps1/PreventWuhanInfection/en.aspx)
 
 <Route author="KeiLongW" example="/coronavirus/mogov-2019ncov/ch" path="/coronavirus/mogov-2019ncov/:lang" paramsDesc={['Language']} />
 
@@ -440,14 +440,10 @@ It is recommended to use with clipping tools such as Notion Web Clipper.
   |    all   | development | design | operation | product | other | marketing | sales |
 </Route>
 
 ## RSSHub {#rsshub}
 
-### Internal Media Proxy {#rsshub-internal-media-proxy}
-
-<Route author="TonyRL" example="/rsshub/m/key/https%3A%2F%2Fdocs.rsshub.app%2Fimg%2Flogo.png" path="/rsshub/m/:key/:url" paramsDesc={['Media Proxy Key', '`encodeURIComponent`ed URL address']} configRequired="1" />
-
 ### Transformation - HTML {#rsshub-transformation-html}
 
 Pass URL and transformation rules to convert HTML/JSON into RSS.
 
 Specify options (in the format of query string) in parameter `routeParams` parameter to extract data from HTML.
diff --git a/website/docs/routes/social-media.mdx b/website/docs/routes/social-media.mdx
index 541f207c34..50fef92932 100644
--- a/website/docs/routes/social-media.mdx
+++ b/website/docs/routes/social-media.mdx
@@ -782,11 +782,11 @@ If the instance address is not `mastodon.social` or `pawoo.net`, then the route
 ### Channel with long videos and file support {#telegram-channel-with-long-videos-and-file-support}
 
 <Route author="synchrone" example="/telegram/channel/telegram" path="/telegram/channel/:username" paramsDesc={['Channel name, without @']} configRequired="1" />
 
 :::warning
-This route requires user-based `TELEGRAM_SESSION` which can be acquired and `MEDIA_PROXY_KEY`.
+This route requires user-based `TELEGRAM_SESSION`.
 :::
 
 ### Sticker Pack {#telegram-sticker-pack}
 
 <Route author="DIYgod" example="/telegram/stickerpack/DIYgod" path="/telegram/stickerpack/:name" paramsDesc={['Sticker Pack name, available in the sharing URL']} />
diff --git a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md
index bab6a0e3e5..a68ae55ead 100644
--- a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md
+++ b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md
@@ -194,12 +194,10 @@ RSSHub 支持使用访问密钥 / 码进行访问控制。开启将会激活全
 
 `FILTER_REGEX_ENGINE`: 控制 [通用参数 -> 内容过滤](/zh/parameter#内容过滤) 使用的正则引擎。可选`[re2, regexp]`，默认`re2`。我们推荐公开实例不要调整这个选项，这个选项目前主要用于向后兼容。
 
 `ALLOW_USER_SUPPLY_UNSAFE_DOMAIN`: 允许用户为路由提供域名作为参数。建议公共实例不要调整此选项，开启后可能会导致 [服务端请求伪造（SSRF）](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
 
-`MEDIA_PROXY_KEY`: 内置多媒体代理的访问密钥
-
 ## 其他应用配置
 
 `DISALLOW_ROBOT`: 阻止搜索引擎收录，默认开启，设置 false 或 0 关闭
 
 `ENABLE_CLUSTER`: 是否开启集群模式，默认 `false`

Vulnerable code:         mediaProxyKey?: string;
            mediaProxyKey: envs.MEDIA_PROXY_KEY,
    '/m/:key/:url': ['TonyRL'],
// @ts-nocheck
import got from '@/utils/got';
import { config } from '@/config';
const { getDomain } = require('tldts');
const { refererMap } = require('./referer-map');

export default async (ctx) => {
    if (!config.feature.mediaProxyKey) {
        throw new Error('Internal media proxy is disabled.');
    }

    const key = ctx.req.param('key');
    if (key !== config.feature.mediaProxyKey) {
        throw new Error('Invalid media proxy key.');
    }

    const url = decodeURIComponent(ctx.req.param('url'));
    const requestUrl = new URL(url);
    const { hostname, origin } = requestUrl;

    const domain = getDomain(hostname);

    let referer = refererMap.get(domain);
    referer ||= origin;

    const { headers } = await got.head(url, {
        headers: {
            referer,
        },
    });

    const cacheControl = headers['cache-control'];
    const contentType = headers['content-type'];
    const contentLength = headers['content-length'];

    if (!contentType.startsWith('image/') || headers.server === 'RSSHub') {
        return ctx.redirect(url);
    }

    ctx.set({
        'cache-control': cacheControl || `public, max-age=${config.cache.contentExpire}`,
        'content-length': contentLength,
        'content-type': contentType,
        server: 'RSSHub',
    });

    ctx.body = await got.stream(url, {
        headers: {
            referer,
        },
    });
};
    router.get('/m/:key/:url', './media');
    const useWeb = ctx.req.param('routeParams') || !(config.telegram.session && config.feature.mediaProxyKey);
    if (ctx.req.param('key') !== config.feature.mediaProxyKey) {
        throw new Error('Invalid key');
    }

    const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}/${config.feature.mediaProxyKey}/`;
            ctx.body = `<script>window.location = '/weibo/timeline/${uid}${routeParams ? `/${routeParams}` : ''}'</script>`;
`MEDIA_PROXY_KEY`: the access key for internal media proxy.

### Macao Pagina Electrónica Especial Contra Epidemias: What’s New {#corona-virus-disease-2019-macao-pagina-electr%C3%B3nica-especial-contra-epidemias-what-s-new}
### Internal Media Proxy {#rsshub-internal-media-proxy}

<Route author="TonyRL" example="/rsshub/m/key/https%3A%2F%2Fdocs.rsshub.app%2Fimg%2Flogo.png" path="/rsshub/m/:key/:url" paramsDesc={['Media Proxy Key', '`encodeURIComponent`ed URL address']} configRequired="1" />

This route requires user-based `TELEGRAM_SESSION` which can be acquired and `MEDIA_PROXY_KEY`.
`MEDIA_PROXY_KEY`: 内置多媒体代理的访问密钥

Fixed code:     const useWeb = ctx.req.param('routeParams') || !config.telegram.session;
    const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}`;
            ctx.html(`<script>window.location = '/weibo/timeline/${uid}${routeParams ? `/${routeParams}` : ''}'</script>`);
### Macao Pagina Electrónica Especial Contra Epidemias: What’s New {#corona-virus-disease-2019-macao-pagina-electronica-especial-contra-epidemias-what-s-new}
This route requires user-based `TELEGRAM_SESSION`.