CVE-2023-22493: Server Side Request Forgery (SSRF) in RSSHub

- 3 mins

Description

An attacker can exploit this vulnerability by sending a request to the affected routes with a malicious URL. For example, if an attacker controls the ATTACKER.HOST domain, they can send a request to affected routes with the value set to ATTACKER.HOST%2F%23. The %2F and %23 characters are URL-encoded versions of the forward-slash (/) and pound (#) characters, respectively. In this context, an attacker could use those characters to append the base URL (i.e. https://${input}.defined.host) to be modified to https://ATTACKER.HOST/#.defined.host. This will cause the server to send a request to the attacker-controlled domain, allowing the attacker to potentially gain access to sensitive information or perform further attacks on the server. 1

Impact

An attacker could 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.

Mitigation

To mitigate this vulnerability, validate the user-supplied value in the parameter for concatenation as a host to ensure that they do not contain malicious values. For example:

  1. Split the input string into an array of strings using the “.” character as the separator.
  2. Check if each element in the array is a valid (sub)-domain. A subdomain name must meet RFC 1034 criteria:
    • Be at least 1 character long or 63 characters or less.
    • Consist only of alphanumeric characters, and hyphens.
    • Not start or end with a hyphen (“-”).

Here’s an example of an approach with a validation function:

function isValidHost(input) {
  const parts = input.split('.');
  const regex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$/;

  return parts.every((part) => regex.test(part));
}

// subd0main: true
// -subd0main: false
// sub-d0main: true
// subd0main-: false
// sub.d0main: true
// sub-.d0main: false
// s: true
// -: false
// 0: true
// s-: false
// s-u: true
// su: true

The input string is split into an array of parts using the .split() method, with '.' as the delimiter. This will give you an array of subdomains, such as ['some', 'sub', 'domain'] for the input 'some.sub.domain'.

A regex pattern is defined to match the previously mentioned criteria. The regular expression allows for a maximum of 63 characters (including the hyphens) per subdomain, except the first & last characters, which can be a letter or a number but not a hyphen.

The .every() method is used to check if every element in the parts array satisfies the condition specified by the callback function. The callback function uses the .test() method of the RegExp object to test if each element matches the regular expression. If all elements in the array pass the test, the .every() method will return true. If any element fails the test, the .every() method will return false.

Implementing the above functionality is part of a subdomain’s multi-level validation. For one level subdomain, there is no need to give expression to split the input into an array of parts using the .split() method.

function isValidHost(input) {
  const regex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$/;

  return regex.test(input);
}

See reference2 for the details.

Timeline

References