CVE-2022-23071 (published 2022-06-19T11:15Z)

In Recipes, versions 0.9.1 through 1.2.5 are vulnerable to Server Side Request Forgery (SSRF), in the “Import Recipe” functionality. When an attacker enters the localhost URL, a low privileged attacker can access/read the internal file system to access sensitive information.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/TandoorRecipes/recipes
commit_details: repo_url: https://github.com/TandoorRecipes/recipes
author: vabene1111
author_date: 2022-05-17 18:04:43+02:00
msg: added url validation to all server requests
repository: repo_url: https://github.com/TandoorRecipes/recipes
repo_name: TandoorRecipes/recipes
description: Application for managing recipes, planning meals, building shopping lists and much much more!
repo_language: HTML
owner: TandoorRecipes
file_changes: filename: cookbookapp.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ from gettext import gettext as _
 from io import BytesIO
 
 import requests
+import validators
 import yaml
 
 from cookbook.helper.ingredient_parser import IngredientParser
@@ -59,8 +60,10 @@ class CookBookApp(Integration):
 
         if len(images) > 0:
             try:
-                response = requests.get(images[0])
-                self.import_recipe_image(recipe, BytesIO(response.content))
+                url = images[0]
+                if validators.url(url, public=True):
+                    response = requests.get(url)
+                    self.import_recipe_image(recipe, BytesIO(response.content))
             except Exception as e:
                 print('failed to import image ', str(e))
 

programming_language: Python
filename: cookmate.py
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ from io import BytesIO
 from gettext import gettext as _
 
 import requests
+import validators
 from lxml import etree
 
 from cookbook.helper.ingredient_parser import IngredientParser
@@ -64,7 +65,9 @@ class Cookmate(Integration):
 
         if recipe_xml.find('imageurl') is not None:
             try:
-                response = requests.get(recipe_xml.find('imageurl').text.strip())
+                url = recipe_xml.find('imageurl').text.strip()
+                if validators.url(url, public=True):
+                    response = requests.get(url)
                 self.import_recipe_image(recipe, BytesIO(response.content))
             except Exception as e:
                 print('failed to import image ', str(e))

programming_language: Python
filename: recettetek.py
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ from io import BytesIO
 from zipfile import ZipFile
 
 import requests
+import validators
 
 from django.utils.translation import gettext as _
 from cookbook.helper.image_processing import get_filetype
@@ -123,11 +124,13 @@ class RecetteTek(Integration):
                         self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)), filetype=get_filetype(image_file_name))
             else:
                 if file['originalPicture'] != '':
-                    response = requests.get(file['originalPicture'])
-                    if imghdr.what(BytesIO(response.content)) is not None:
-                        self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
-                    else:
-                        raise Exception("Original image failed to download.")
+                    url = file['originalPicture']
+                    if validators.url(url, public=True):
+                        response = requests.get(url)
+                        if imghdr.what(BytesIO(response.content)) is not None:
+                            self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
+                        else:
+                            raise Exception("Original image failed to download.")
         except Exception as e:
             print(recipe.name, ': failed to import image ', str(e))
 

programming_language: Python
filename: recipesage.py
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import json
 from io import BytesIO
 
 import requests
+import validators
 
 from cookbook.helper.ingredient_parser import IngredientParser
 from cookbook.integration.integration import Integration
@@ -51,8 +52,10 @@ class RecipeSage(Integration):
 
         if len(file['image']) > 0:
             try:
-                response = requests.get(file['image'][0])
-                self.import_recipe_image(recipe, BytesIO(response.content))
+                url = file['image'][0]
+                if validators.url(url, public=True):
+                    response = requests.get(url)
+                    self.import_recipe_image(recipe, BytesIO(response.content))
             except Exception as e:
                 print('failed to import image ', str(e))
 

programming_language: Python
filename: dropbox.py
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,8 @@ import os
 from datetime import datetime
 
 import requests
+import validators
+
 from cookbook.models import Recipe, RecipeImport, SyncLog
 from cookbook.provider.provider import Provider
 
@@ -104,9 +106,11 @@ class Dropbox(Provider):
             recipe.link = Dropbox.get_share_link(recipe)
             recipe.save()
 
-        response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
+        url = recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.')
+        if validators.url(url, public=True):
+            response = requests.get(url)
 
-        return io.BytesIO(response.content)
+            return io.BytesIO(response.content)
 
     @staticmethod
     def rename_file(recipe, new_name):

programming_language: Python
filename: nextcloud.py
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,7 @@ import tempfile
 from datetime import datetime
 
 import requests
+import validators
 import webdav3.client as wc
 from cookbook.models import Recipe, RecipeImport, SyncLog
 from cookbook.provider.provider import Provider
@@ -92,20 +93,21 @@ class Nextcloud(Provider):
             "Content-Type": "application/json"
         }
 
-        r = requests.get(
-            url,
-            headers=headers,
-            auth=HTTPBasicAuth(
-                recipe.storage.username, recipe.storage.password
+        if validators.url(url, public=True):
+            r = requests.get(
+                url,
+                headers=headers,
+                auth=HTTPBasicAuth(
+                    recipe.storage.username, recipe.storage.password
+                )
             )
-        )
 
-        response_json = r.json()
-        for element in response_json['ocs']['data']:
-            if element['share_type'] == '3':
-                return element['url']
+            response_json = r.json()
+            for element in response_json['ocs']['data']:
+                if element['share_type'] == '3':
+                    return element['url']
 
-        return Nextcloud.create_share_link(recipe)
+            return Nextcloud.create_share_link(recipe)
 
     @staticmethod
     def get_file(recipe):

programming_language: Python
filename: api.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ import uuid
 from collections import OrderedDict
 
 import requests
+import validators
 from PIL import UnidentifiedImageError
 from annoying.decorators import ajax_request
 from annoying.functions import get_object_or_None
@@ -14,7 +15,7 @@ from django.contrib.auth.models import User
 from django.contrib.postgres.search import TrigramSimilarity
 from django.core.exceptions import FieldError, ValidationError
 from django.core.files import File
-from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef, ProtectedError, Q,
+from django.db.models import (Case, Count, Exists, OuterRef, ProtectedError, Q,
                               Subquery, Value, When)
 from django.db.models.fields.related import ForeignObjectRel
 from django.db.models.functions import Coalesce, Lower
@@ -24,7 +25,6 @@ from django.urls import reverse
 from django.utils.translation import gettext as _
 from django_scopes import scopes_disabled
 from icalendar import Calendar, Event
-from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me
 from requests.exceptions import MissingSchema
 from rest_framework import decorators, status, viewsets
 from rest_framework.exceptions import APIException, PermissionDenied
@@ -34,6 +34,7 @@ from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
 from rest_framework.response import Response
 from rest_framework.viewsets import ViewSetMixin
 from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
+from validators import ValidationFailure
 
 from cookbook.helper.HelperFunctions import str2bool
 from cookbook.helper.image_processing import handle_image
@@ -43,7 +44,6 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
                                                group_required)
 from cookbook.helper.recipe_html_import import get_recipe_from_source
 from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
-from cookbook.helper.recipe_url_import import get_from_scraper
 from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
 from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
                              FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
@@ -774,16 +774,18 @@ class RecipeViewSet(viewsets.ModelViewSet):
         if serializer.is_valid():
             serializer.save()
             image = None
-            filetype = ".jpeg" # fall-back to .jpeg, even if wrong, at least users will know it's an image and most image viewers can open it correctly anyways
+            filetype = ".jpeg"  # fall-back to .jpeg, even if wrong, at least users will know it's an image and most image viewers can open it correctly anyways
 
             if 'image' in serializer.validated_data:
                 image = obj.image
                 filetype = mimetypes.guess_extension(serializer.validated_data['image'].content_type) or filetype
             elif 'image_url' in serializer.validated_data:
                 try:
-                    response = requests.get(serializer.validated_data['image_url'])
-                    image = File(io.BytesIO(response.content))
-                    filetype = mimetypes.guess_extension(response.headers['content-type']) or filetype
+                    url = serializer.validated_data['image_url']
+                    if validators.url(url, public=True):
+                        response = requests.get(url)
+                        image = File(io.BytesIO(response.content))
+                        filetype = mimetypes.guess_extension(response.headers['content-type']) or filetype
                 except UnidentifiedImageError as e:
                     print(e)
                     pass
@@ -1188,7 +1190,13 @@ def recipe_from_source(request):
     # in manual mode request complete page to return it later
     if url:
         try:
-            data = requests.get(url, headers=external_request_headers).content
+            if validators.url(url, public=True):
+                data = requests.get(url, headers=external_request_headers).content
+            else:
+                return JsonResponse({
+                    'error': True,
+                    'msg': _('Invalid Url')
+                }, status=400)
         except requests.exceptions.ConnectionError:
             return JsonResponse({
                 'error': True,
@@ -1199,6 +1207,7 @@ def recipe_from_source(request):
                 'error': True,
                 'msg': _('Bad URL Schema.')
             }, status=400)
+
     recipe_json, recipe_tree, recipe_html, recipe_images = get_recipe_from_source(data, url, request)
     if len(recipe_tree) == 0 and len(recipe_json) == 0:
         return JsonResponse({

programming_language: Python
filename: requirements.txt
change_type: ModificationType.MODIFY
diff: @@ -43,3 +43,4 @@ python-ldap==3.4.0
 django-auth-ldap==4.0.0
 pytest-factoryboy==2.1.0
 pyppeteer==1.0.2
+validators==0.19.0
\ No newline at end of file

programming_language: Batchfile