-
-
Notifications
You must be signed in to change notification settings - Fork 814
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* change ALLOW_SIGNUP to default to false * add 1.4.0 tag for OIDC docs * new notes on security inline with security/policy review * safer transport for external requests * fix linter errors * docs: Tidy up wording/formatting * fix request errors * whoops * fix implementation with std lib * format * Remove check on netloc_parts. It only includes URL after any @ --------- Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com> Co-authored-by: Brendan <b.oconnell14@gmail.com>
- Loading branch information
1 parent
737a370
commit 2a3463b
Showing
11 changed files
with
180 additions
and
54 deletions.
There are no files selected for viewing
2 changes: 2 additions & 0 deletions
2
docs/docs/documentation/getting-started/authentication/oidc.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
docs/docs/documentation/getting-started/installation/security.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
--- | ||
tags: | ||
- Security | ||
--- | ||
|
||
# Security Considerations | ||
|
||
This page is a collection of security considerations for Mealie. It mostly deals with reported issues and how it's possible to mitigate them. Note that this page is for you to use as a guide for how secure you want to make your deployment. It's important to note that most of these will not apply to you, if you: | ||
|
||
1. Run behind a VPN | ||
2. Use a strong password | ||
3. Disable Sign-Ups | ||
4. Don't host for malicious users | ||
|
||
Use your best judgement when deciding what to do. | ||
|
||
## Denial of Service | ||
|
||
By default, the API is **not** rate limited. This leaves Mealie open to a potential **Denial of Service Attack**. While it's possible to perform a **Denial of Service Attack** on any endpoint, there are a few key endpoints that are more vulnerable than others. | ||
|
||
- `/api/recipes/create-url` | ||
- `/api/recipes/{id}/image` | ||
|
||
These endpoints are used to scrape data based off a user provided URL. It is possible for a malicious user to issue multiple requests to download an arbitrarily large external file (e.g a Debian ISO) and sufficiently saturate a CPU assigned to the container. While we do implement some protections against this by chunking the response, and using a timeout strategy, it's still possible to overload the CPU if an attacker issues multiple requests concurrently. | ||
|
||
### Mitigation | ||
|
||
If you'd like to mitigate this risk, we suggest that you rate limit the API in general, and apply strict rate limits to these endpoints. You can do this by utilizing a reverse proxy. See the following links to get started: | ||
|
||
- [Traefik](https://doc.traefik.io/traefik/middlewares/http/ratelimit/) | ||
- [Nginx](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html) | ||
- [Caddy](https://caddyserver.com/docs/modules/http.handlers.rate_limit) | ||
|
||
## Server Side Request Forgery | ||
|
||
- `/api/recipes/create-url` | ||
- `/api/recipes/{id}/image` | ||
|
||
Given the nature of these APIs it's possible to perform a **Server Side Request Forgery** attack. This is where a malicious user can issue a request to an internal network resource, and potentially exfiltrate data. We _do_ perform some checks to mitigate access to resources within your network but at the end of the day, users of Mealie are allowed to trigger HTTP requests on **your server**. | ||
|
||
### Mitigation | ||
|
||
If you'd like to mitigate this risk, we suggest that you isolate the container that Mealie is running in to ensure that it's access to internal resources is limited only to what is required. _Note that Mealie does require access to the internet for recipe imports._ You might consider isolating Mealie from your home network entirely and only allowing access to the external internet. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from .transport import AsyncSafeTransport, ForcedTimeoutException, InvalidDomainError | ||
|
||
__all__ = [ | ||
"AsyncSafeTransport", | ||
"ForcedTimeoutException", | ||
"InvalidDomainError", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import ipaddress | ||
import logging | ||
import socket | ||
|
||
import httpx | ||
|
||
|
||
class ForcedTimeoutException(Exception): | ||
""" | ||
Raised when a request takes longer than the timeout value. | ||
""" | ||
|
||
... | ||
|
||
|
||
class InvalidDomainError(Exception): | ||
""" | ||
Raised when a request is made to a local IP address. | ||
""" | ||
|
||
... | ||
|
||
|
||
class AsyncSafeTransport(httpx.AsyncBaseTransport): | ||
""" | ||
A wrapper around the httpx transport class that enforces a timeout value | ||
and that the request is not made to a local IP address. | ||
""" | ||
|
||
timeout: int = 15 | ||
|
||
def __init__(self, log: logging.Logger | None = None, **kwargs): | ||
self.timeout = kwargs.pop("timeout", self.timeout) | ||
self._wrapper = httpx.AsyncHTTPTransport(**kwargs) | ||
self._log = log | ||
|
||
async def handle_async_request(self, request): | ||
# override timeout value for _all_ requests | ||
request.extensions["timeout"] = httpx.Timeout(self.timeout, pool=self.timeout).as_dict() | ||
|
||
# validate the request is not attempting to connect to a local IP | ||
# This is a security measure to prevent SSRF attacks | ||
|
||
ip: ipaddress.IPv4Address | ipaddress.IPv6Address | None = None | ||
|
||
netloc = request.url.netloc.decode() | ||
if ":" in netloc: # Either an IP, or a hostname:port combo | ||
netloc_parts = netloc.split(":") | ||
|
||
netloc = netloc_parts[0] | ||
|
||
try: | ||
ip = ipaddress.ip_address(netloc) | ||
except ValueError: | ||
if self._log: | ||
self._log.debug(f"failed to parse ip for {netloc=} falling back to domain resolution") | ||
pass | ||
|
||
# Request is a domain or a hostname. | ||
if not ip: | ||
if self._log: | ||
self._log.debug(f"resolving IP for domain: {netloc}") | ||
|
||
ip_str = socket.gethostbyname(netloc) | ||
ip = ipaddress.ip_address(ip_str) | ||
|
||
if self._log: | ||
self._log.debug(f"resolved IP for domain: {netloc} -> {ip}") | ||
|
||
if ip.is_private: | ||
if self._log: | ||
self._log.warning(f"invalid request on local resource: {request.url} -> {ip}") | ||
raise InvalidDomainError(f"invalid request on local resource: {request.url} -> {ip}") | ||
|
||
return await self._wrapper.handle_async_request(request) | ||
|
||
async def aclose(self): | ||
await self._wrapper.aclose() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.