CVE-2021-21377: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: OMERO.web is open source Django-based software for managing microscopy imaging. OMERO.web before version 5.9.0 supports redirection to a given URL after performing login or switching the group context. These URLs are not validated, allowing redirection to untrusted sites. OMERO.web 5.9.0 adds URL validation before redirecting. External URLs are not considered valid, unless specified in the omero.web.redirect_allowed_hosts setting.
CWE description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
CVSS v3 base score: 5.4
Published: 2021-03-23T16:15Z
Repository: https://github.com/ome/omero-web
Language: Python
Files changed: CHANGELOG.md, omeroweb/decorators.py, omeroweb/settings.py, omeroweb/webclient/decorators.py, omeroweb/webclient/forms.py, omeroweb/webclient/static/webclient/javascript/ome.tree.js, omeroweb/webclient/templates/webclient/base/base_container.html, omeroweb/webclient/templates/webclient/base/includes/toolbar_forms.html, omeroweb/webclient/templates/webclient/data/containers.html, omeroweb/webclient/templates/webclient/public/public.html, omeroweb/webclient/views.py, omeroweb/webgateway/templates/webgateway/core_html.html, omeroweb/webgateway/views.py
Commit date: 2021-03-17 08:35:48 +0000
Commit message: SV commits

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4d6f6312..ec753baf2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,10 +20,14 @@
 - Remove broken 'Create Shares' dialog ([#265](https://github.com/ome/omero-web/pull/265))
 - Fix API ?childCount=true when zero objects found ([#249](https://github.com/ome/omero-web/pull/249))
 - Fix partial loading of annotations ([#256](https://github.com/ome/omero-web/pull/256))
 - Fix ignored limit in webgateway/table endpoint ([#268](https://github.com/ome/omero-web/pull/268))
 
+- Security vulnerability fixes for
+  [2021-SV1](https://www.openmicroscopy.org/security/advisories/2021-SV1-user-context/),
+  [2021-SV2](https://www.openmicroscopy.org/security/advisories/2021-SV2-url-validation/)
+
 5.8.1 (September 2020)
 ----------------------
 
 ## Bug fixes
 
diff --git a/omeroweb/decorators.py b/omeroweb/decorators.py
index 8ab85d4b4..b7cf30177 100644
--- a/omeroweb/decorators.py
+++ b/omeroweb/decorators.py
@@ -580,11 +580,12 @@ class render_response(object):
         else:
             return super(render_response, self).getattr(name)
 
     def prepare_context(self, request, context, *args, **kwargs):
         """ Hook for adding additional data to the context dict """
-        pass
+        context["html"] = context.get("html", {})
+        context["html"]["meta_referrer"] = settings.HTML_META_REFERRER
 
     def __call__(ctx, f):
         """ Here we wrap the view method f and return the wrapped method """
 
         def wrapper(request, *args, **kwargs):
diff --git a/omeroweb/settings.py b/omeroweb/settings.py
index 521f05d36..b7f6446e9 100644
--- a/omeroweb/settings.py
+++ b/omeroweb/settings.py
@@ -506,10 +506,16 @@ CUSTOM_SETTINGS_MAPPINGS = {
         "SESSION_COOKIE_NAME",
         None,
         leave_none_unset,
         "The name to use for session cookies",
     ],
+    "omero.web.session_cookie_path": [
+        "SESSION_COOKIE_PATH",
+        None,
+        leave_none_unset,
+        "The path to use for session cookies",
+    ],
     "omero.web.session_cookie_secure": [
         "SESSION_COOKIE_SECURE",
         "false",
         parse_boolean,
         (
@@ -864,10 +870,20 @@ CUSTOM_SETTINGS_MAPPINGS = {
             'For example: ``\'{"redirect": ["webindex"], "viewname":'
             ' "load_template", "args":["userdata"], "query_string":'
             ' {"experimenter": -1}}\'``'
         ),
     ],
+    "omero.web.redirect_allowed_hosts": [
+        "REDIRECT_ALLOWED_HOSTS",
+        "[]",
+        json.loads,
+        (
+            "If you wish to allow redirects to an external site, "
+            "the domains must be listed here. "
+            'For example ["openmicroscopy.org"].'
+        ),
+    ],
     "omero.web.login.show_client_downloads": [
         "SHOW_CLIENT_DOWNLOADS",
         "true",
         parse_boolean,
         ("Whether to link to official client downloads on the login page"),
@@ -1020,10 +1036,24 @@ CUSTOM_SETTINGS_MAPPINGS = {
         (
             "If True, cors_origin_whitelist will not be used and all origins "
             "will be authorized to make cross-site HTTP requests."
         ),
     ],
+    "omero.web.html_meta_referrer": [
+        "HTML_META_REFERRER",
+        "origin-when-crossorigin",
+        str,
+        (
+            "Default content for the HTML Meta referrer tag. "
+            "See https://www.w3.org/TR/referrer-policy/#referrer-policies for "
+            "allowed values and https://caniuse.com/#feat=referrer-policy for "
+            "browser compatibility. "
+            "Warning: Internet Explorer 11 does not support the default value "
+            'of this setting, you may want to change this to "origin" after '
+            "reviewing the linked documentation."
+        ),
+    ],
     "omero.web.x_frame_options": [
         "X_FRAME_OPTIONS",
         "SAMEORIGIN",
         str,
         "Whether to allow OMERO.web to be loaded in a frame.",
diff --git a/omeroweb/webclient/decorators.py b/omeroweb/webclient/decorators.py
index 591fa878d..eea31b00d 100644
--- a/omeroweb/webclient/decorators.py
+++ b/omeroweb/webclient/decorators.py
@@ -33,11 +33,10 @@ from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import NoReverseMatch
 
 from omeroweb.webclient.forms import GlobalSearchForm
 from omeroweb.utils import reverse_with_params
-from omeroweb.webgateway.marshal import eventContextMarshal
 
 logger = logging.getLogger(__name__)
 
 
 class login_required(omeroweb.decorators.login_required):
@@ -114,10 +113,12 @@ class render_response(omeroweb.decorators.render_response):
         additional cost.
         We also process some values from settings and add these to the
         context.
         """
 
+        super(render_response, self).prepare_context(request, context, *args, **kwargs)
+
         # we expect @login_required to pass us 'conn', but just in case...
         if "conn" not in kwargs:
             return
         conn = kwargs["conn"]
 
@@ -132,11 +133,10 @@ class render_response(omeroweb.decorators.render_response):
 
         context.setdefault("ome", {})  # don't overwrite existing ome
         public_user = omeroweb.decorators.is_public_user(request)
         if public_user is not None:
             context["ome"]["is_public_user"] = public_user
-        context["ome"]["eventContext"] = eventContextMarshal(conn.getEventContext())
         context["ome"]["user"] = conn.getUser
         context["ome"]["user_id"] = request.session.get("user_id", conn.getUserId())
         context["ome"]["group_id"] = request.session.get("group_id", None)
         context["ome"]["active_group"] = request.session.get(
             "active_group", conn.getEventContext().groupId
diff --git a/omeroweb/webclient/forms.py b/omeroweb/webclient/forms.py
index f31c95a5e..6ac495d2a 100644
--- a/omeroweb/webclient/forms.py
+++ b/omeroweb/webclient/forms.py
@@ -35,11 +35,10 @@ from django.core.urlresolvers import reverse
 from omeroweb.custom_forms import NonASCIIForm
 from .custom_forms import MetadataModelChoiceField
 from .custom_forms import AnnotationModelMultipleChoiceField
 from .custom_forms import ObjectModelMultipleChoiceField
 from omeroweb.webadmin.custom_forms import ExperimenterModelMultipleChoiceField
-from omeroweb.webadmin.custom_forms import GroupModelMultipleChoiceField
 from omeroweb.webadmin.custom_forms import GroupModelChoiceField
 from omeroweb.webclient.webclient_utils import formatPercentFraction
 
 logger = logging.getLogger(__name__)
 
@@ -125,27 +124,10 @@ class ShareForm(NonASCIIForm):
             if time.mktime(date.timetuple()) <= time.time():
                 raise forms.ValidationError("Expiry date must be in the future.")
         return self.cleaned_data["expiration"]
 
 
-class BasketShareForm(ShareForm):
-    def __init__(self, *args, **kwargs):
-        super(BasketShareForm, self).__init__(*args, **kwargs)
-
-        try:
-            self.fields["image"] = GroupModelMultipleChoiceField(
-                queryset=kwargs["initial"]["images"],
-                initial=kwargs["initial"]["selected"],
-                widget=forms.SelectMultiple(attrs={"size": 10}),
-            )
-        except Exception:
-            self.fields["image"] = GroupModelMultipleChoiceField(
-                queryset=kwargs["initial"]["images"],
-                widget=forms.SelectMultiple(attrs={"size": 10}),
-            )
-
-
 class ContainerForm(NonASCIIForm):
 
     name = forms.CharField(max_length=250, widget=forms.TextInput(attrs={"size": 45}))
     description = forms.CharField(
         widget=forms.Textarea(attrs={"rows": 2, "cols": 49}), required=False
diff --git a/omeroweb/webclient/static/webclient/javascript/ome.tree.js b/omeroweb/webclient/static/webclient/javascript/ome.tree.js
index eefa1b250..204746f38 100644
--- a/omeroweb/webclient/static/webclient/javascript/ome.tree.js
+++ b/omeroweb/webclient/static/webclient/javascript/ome.tree.js
@@ -1149,12 +1149,12 @@ $(function() {
                 // List of permissions related disabling
                 // use canLink, canDelete etc classes on each node to enable/disable right-click menu
 
                 var userId = WEBCLIENT.active_user.id,
                     // admin may be viewing a Group that they are not a member of
-                    memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1,
-                    writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1,
+                    memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1,
+                    writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1,
                     allMembers = userId === -1,
                     // canCreate if looking at your own data or 'All Members' OR User's data && writeOwned
                     canCreate = (userId === WEBCLIENT.USER.id || (allMembers && memberOfGroup) ||
                         (!allMembers && writeOwned)),
                     canLink = OME.nodeHasPermission(node, 'canLink'),
diff --git a/omeroweb/webclient/templates/webclient/base/base_container.html b/omeroweb/webclient/templates/webclient/base/base_container.html
index 6efb62e30..b30c11c47 100644
--- a/omeroweb/webclient/templates/webclient/base/base_container.html
+++ b/omeroweb/webclient/templates/webclient/base/base_container.html
@@ -164,11 +164,11 @@
         var WEBCLIENT = {};
 
         WEBCLIENT.active_group_id = {{ active_group.id }};
         WEBCLIENT.USER = {'id': {{ ome.user.id }}, 'fullName': "{{ ome.user.getFullName }}"};
         WEBCLIENT.active_user = {'id': {{ ome.user_id }}, 'fullName': "{{ active_user.getFullName }}"};
-        WEBCLIENT.eventContext = {{ ome.eventContext|json_dumps|safe }};
+        WEBCLIENT.member_of_groups = {{ member_of_groups|json_dumps|safe }};
         WEBCLIENT.isAdmin = {% if ome.user.isAdmin %}true{% else %}false{% endif %};
         WEBCLIENT.CAN_CREATE = {{ ome.can_create|json_dumps|safe }};
         WEBCLIENT.current_admin_privileges = {{ current_admin_privileges|json_dumps|safe }};
         WEBCLIENT.leader_of_groups = {{ leader_of_groups|json_dumps|safe }};
 
diff --git a/omeroweb/webclient/templates/webclient/base/includes/toolbar_forms.html b/omeroweb/webclient/templates/webclient/base/includes/toolbar_forms.html
index ef58b6f5e..4c8d7dd13 100644
--- a/omeroweb/webclient/templates/webclient/base/includes/toolbar_forms.html
+++ b/omeroweb/webclient/templates/webclient/base/includes/toolbar_forms.html
@@ -1,11 +1,11 @@
 {% load i18n %}
 {% load common_filters %}
 
 {% comment %}
 <!--
-  Copyright (C) 2011 University of Dundee & Open Microscopy Environment.
+  Copyright (C) 2011-2021 University of Dundee & Open Microscopy Environment.
   All rights reserved.
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as
   published by the Free Software Foundation, either version 3 of the
   License, or (at your option) any later version.
@@ -23,84 +23,35 @@
 
 <script type="text/javascript">
 if (typeof OME === "undefined") { OME={}; }
 
     OME.createShare = function() {
-
-        var productListQuery = [];
-
-        // we do inst.get_selected() here, since we then get objects
-        // instead of ids for some reason?
-        var inst = $.jstree.reference('#dataTree');
-        data = inst.get_selected(true);
-        data.forEach(function(node){
-            productListQuery.push(node.type + "=" + node.data.id);
-        });
-
-        var query = '{% url 'manage_action_containers' "add" "share" %}' + "?"+productListQuery.join("&");
         $("#create_share_form").dialog("open");
-        $("#create_share_form").attr("action", query)
-        $("#create_share_form").load(query);
         return false;
     }
 
     $(document).ready(function(){
 
-        // AJAX handling of create-discussion form
-        $("#create_share_form").ajaxForm({
-            success: function(html) {
-                if (html.indexOf("shareId") > -1) {
-                    var shareId = html.replace("shareId:", "");
-                    $("#create_share_form").dialog( "close" );
-                    $("#shareCreatedId").text(shareId);
-                    $("#share_dialog_form").dialog("open").show();
-                } else {
-                    $("#create_share_form").html(html);
-                }
-            },
-        });
-
-        $("#share_dialog_form").dialog({
-            autoOpen: false,
-            resizable: true,
-            height: 150,
-            width:300,
-            modal: true,
-            buttons: {
-                "OK": function() {
-                    $( this ).dialog( "close" );
-                }
-            }
-        });
-
         $("#create_share_form").dialog({
+            title: "Shares not supported",
             autoOpen: false,
             resizable: true,
-            height: 600,
+            height: 250,
             width:450,
             modal: true,
             buttons: {
-                "Accept": function() {
-                    // simply submit the form
-                    $("#create_share_form").submit();
-                },
-                "Cancel": function() {
+                "OK": function() {
                     $( this ).dialog( "close" );
                 }
             }
         });
-
-
     });
 </script>
 
 
-
-<!-- hidden form for creating share - shown in dialog & loaded by AJAX -->
-<form id="create_share_form" action="#" method="post" title="Create Share" class="standard_form">{% csrf_token %}
-</form>
-
-<form id="share_dialog_form" action="#" title="Create Share" style="display:none">
-    <p style="font-size: 120%; font-weight: bold">
-        Share <span id="shareCreatedId"></span> was created successfully.
+<!-- hidden dialog -->
+<div id="create_share_form" style="display:none">
+    <p>Share functionality is no longer supported.</p>
+    <p>Please see <a target="_blank" href="https://www.openmicroscopy.org/omero/features/share/">Sharing your data in OMERO</a>
+        for alternative workflows.
     </p>
-</form>
+</div>
diff --git a/omeroweb/webclient/templates/webclient/data/containers.html b/omeroweb/webclient/templates/webclient/data/containers.html
index e6e3d8b7f..835962a9f 100644
--- a/omeroweb/webclient/templates/webclient/data/containers.html
+++ b/omeroweb/webclient/templates/webclient/data/containers.html
@@ -206,13 +206,13 @@
         OME.handleNewContainer = function(container_type) {
 
             // If we are filtering to show another user's data,
             // we 'should' have writeOwned privilege
 
-            var writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1;
+            var writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1;
             var $f = $("#new-container-form");
-            var memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1;
+            var memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1;
 
             // clear fields
             $("input[name='owner']", $f).val("");
             var new_container_name = $("input[name='name']", $f).val("");
             var new_container_desc = $("textarea[name='description']", $f).val("");
@@ -287,12 +287,12 @@
                 'createshare':false
             };
 
             // We 'canCreate' top level items, E.g. Project, Dataset, Screen, if the current userId is self or 'All Members'
             var userId = {{ ome.user_id }},
-                memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1,
-                writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1,
+                memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1,
+                writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1,
                 allMembers = userId === -1,
                 // canCreate if looking at your own data or 'All Members' OR User's data with writeOwned
                 canCreate = (userId === WEBCLIENT.USER.id || (allMembers && memberOfGroup) ||
                     (!allMembers && writeOwned));
 
diff --git a/omeroweb/webclient/templates/webclient/public/public.html b/omeroweb/webclient/templates/webclient/public/public.html
index 59e65f008..16c38e022 100644
--- a/omeroweb/webclient/templates/webclient/public/public.html
+++ b/omeroweb/webclient/templates/webclient/public/public.html
@@ -487,11 +487,19 @@
 
 </div>
 
 <div class="left_panel_tree_container">
 
-    <div id="tree_details" class="left_panel_tree">
+    <div style="height: 110px; padding: 15px; box-sizing: border-box;">
+        <p>Creating new shares is no longer supported. Previously created shares are shown below.</p>
+        <p>Please see <a target="_blank" href="https://www.openmicroscopy.org/omero/features/share/">Sharing your data in
+                OMERO</a>
+            for alternative workflows.
+        </p>
+    </div>
+
+    <div id="tree_details" class="left_panel_tree" style="height: calc(100% - 110px)">
         <div class="datashareTree" id="dataTree"></div>
     </div>
 
 </div>
 
diff --git a/omeroweb/webclient/views.py b/omeroweb/webclient/views.py
index ea17b390f..e3972ac09 100755
--- a/omeroweb/webclient/views.py
+++ b/omeroweb/webclient/views.py
@@ -33,10 +33,11 @@ import json
 import re
 import sys
 import warnings
 from past.builtins import unicode
 from future.utils import bytes_to_native_str
+from django.utils.http import is_safe_url
 
 from time import time
 
 from omeroweb.version import omeroweb_buildyear as build_year
 from omeroweb.version import omeroweb_version as omero_version
@@ -65,11 +66,11 @@ from django.views.decorators.http import require_POST
 from django.shortcuts import render
 
 
 from omeroweb.webclient.webclient_utils import _formatReport, _purgeCallback
 from .forms import GlobalSearchForm, ContainerForm
-from .forms import ShareForm, BasketShareForm
+from .forms import ShareForm
 from .forms import ContainerNameForm, ContainerDescriptionForm
 from .forms import CommentAnnotationForm, TagsAnnotationForm
 from .forms import MetadataFilterForm, MetadataDetectorForm
 from .forms import MetadataChannelForm, MetadataEnvironmentForm
 from .forms import MetadataObjectiveForm, MetadataObjectiveSettingsForm
@@ -174,10 +175,21 @@ def get_bool_or_default(request, name, default):
     the arguments provided do not pass basic type validation
     """
     return toBoolean(request.GET.get(name, default))
 
 
+def validate_redirect_url(url):
+    """
+    Returns a URL is safe to redirect to.
+    If url is a different host, not in settings.REDIRECT_ALLOWED_HOSTS
+    we return webclient index URL.
+    """
+    if not is_safe_url(url, allowed_hosts=settings.REDIRECT_ALLOWED_HOSTS):
+        url = reverse("webindex")
+    return url
+
+
 ##############################################################################
 # custom index page
 
 
 @never_cache
@@ -255,10 +267,12 @@ class WebclientLoginView(LoginView):
         if url is None or len(url) == 0:
             try:
                 url = parse_url(settings.LOGIN_REDIRECT)
             except Exception:
                 url = reverse("webindex")
+        else:
+            url = validate_redirect_url(url)
         return HttpResponseRedirect(url)
 
     def handle_not_logged_in(self, request, error=None, form=None):
         """
         Returns a response for failed login.
@@ -333,10 +347,11 @@ def change_active_group(request, conn=None, url=None, **kwargs):
     queries.
     Finally this redirects to the 'url'.
     """
     switch_active_group(request)
     url = url or reverse("webindex")
+    url = validate_redirect_url(url)
     return HttpResponseRedirect(url)
 
 
 def switch_active_group(request, active_group=None):
     """
@@ -532,10 +547,11 @@ def _load_template(request, menu, conn=None, url=None, **kwargs):
     context["page_size"] = settings.PAGE
     context["template"] = template
     context["thumbnails_batch"] = settings.THUMBNAILS_BATCH
     context["current_admin_privileges"] = conn.getCurrentAdminPrivileges()
     context["leader_of_groups"] = conn.getEventContext().leaderOfGroups
+    context["member_of_groups"] = conn.getEventContext().memberOfGroups
 
     return context
 
 
 @login_required()
@@ -2869,51 +2885,10 @@ def manage_action_containers(
             d = dict()
             for e in form.errors.items():
                 d.update({e[0]: unicode(e[1])})
             rdict = {"bad": "true", "errs": d}
             return JsonResponse(rdict)
-    elif action == "add":
-        template = "webclient/public/share_form.html"
-        experimenters = list(conn.getExperimenters())
-        experimenters.sort(key=lambda x: x.getOmeName().lower())
-        if o_type == "share":
-            img_ids = request.GET.getlist("image", request.POST.getlist("image"))
-            if request.method == "GET" and len(img_ids) == 0:
-                return HttpResponse("No images specified")
-            images_to_share = list(conn.getObjects("Image", img_ids))
-            if request.method == "POST":
-                form = BasketShareForm(
-                    initial={"experimenters": experimenters, "images": images_to_share},
-                    data=request.POST.copy(),
-                )
-                if form.is_valid():
-                    images = form.cleaned_data["image"]
-                    message = form.cleaned_data["message"]
-                    expiration = form.cleaned_data["expiration"]
-                    members = form.cleaned_data["members"]
-                    # guests = request.POST['guests']
-                    enable = form.cleaned_data["enable"]
-                    host = "%s?server=%i" % (
-                        request.build_absolute_uri(
-                            reverse("load_template", args=["public"])
-                        ),
-                        int(conn.server_id),
-                    )
-                    shareId = manager.createShare(
-                        host, images, message, members, enable, expiration
-                    )
-                    return HttpResponse("shareId:%s" % shareId)
-            else:
-                initial = {
-                    "experimenters": experimenters,
-                    "images": images_to_share,
-                    "enable": True,
-                    "selected": request.GET.getlist("image"),
-                }
-                form = BasketShareForm(initial=initial)
-        template = "webclient/public/share_form.html"
-        context = {"manager": manager, "form": form}
 
     elif action == "edit":
         # form for editing Shares only
         if o_id is None:
             raise Http404("No share ID")
diff --git a/omeroweb/webgateway/templates/webgateway/core_html.html b/omeroweb/webgateway/templates/webgateway/core_html.html
index b2a992acd..6e3903282 100644
--- a/omeroweb/webgateway/templates/webgateway/core_html.html
+++ b/omeroweb/webgateway/templates/webgateway/core_html.html
@@ -23,10 +23,11 @@
 -->
 {% endcomment %}
 
 <head>
     <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+    <meta name="referrer" content="{{ html.meta_referrer|default:"origin-when-crossorigin" }}">
 
     {% block link %}
 		<link rel="stylesheet" href="{% static "webgateway/css/reset.css"|add:url_suffix %}" type="text/css" />   
 		<link rel="stylesheet" href="{% static "webgateway/css/ome.body.css"|add:url_suffix %}" type="text/css" />
 		<link rel="stylesheet" href="{% static "webclient/css/dusty.css"|add:url_suffix %}" type="text/css" media="screen"/>
diff --git a/omeroweb/webgateway/views.py b/omeroweb/webgateway/views.py
index a7b6167d8..eb3dba2d5 100644
--- a/omeroweb/webgateway/views.py
+++ b/omeroweb/webgateway/views.py
@@ -2958,14 +2958,14 @@ def _table_query(request, fileid, conn=None, query=None, lazy=False, **kwargs):
             offset = int(request.GET.get("offset", 0))
         if not limit:
             limit = (
                 int(request.GET.get("limit"))
                 if request.GET.get("limit") is not None
-                else None
+                else rows
             )
         range_start = offset
-        range_size = kwargs.get("limit", rows)
+        range_size = limit
         range_end = min(rows, range_start + range_size)
 
         if query == "*":
             hits = range(range_start, range_end)
             totalCount = rows

Vulnerable code:         pass
from omeroweb.webgateway.marshal import eventContextMarshal
        context["ome"]["eventContext"] = eventContextMarshal(conn.getEventContext())
from omeroweb.webadmin.custom_forms import GroupModelMultipleChoiceField
class BasketShareForm(ShareForm):
    def __init__(self, *args, **kwargs):
        super(BasketShareForm, self).__init__(*args, **kwargs)

        try:
            self.fields["image"] = GroupModelMultipleChoiceField(
                queryset=kwargs["initial"]["images"],
                initial=kwargs["initial"]["selected"],
                widget=forms.SelectMultiple(attrs={"size": 10}),
            )
        except Exception:
            self.fields["image"] = GroupModelMultipleChoiceField(
                queryset=kwargs["initial"]["images"],
                widget=forms.SelectMultiple(attrs={"size": 10}),
            )


                    memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1,
                    writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1,
        WEBCLIENT.eventContext = {{ ome.eventContext|json_dumps|safe }};
  Copyright (C) 2011 University of Dundee & Open Microscopy Environment.

        var productListQuery = [];

        // we do inst.get_selected() here, since we then get objects
        // instead of ids for some reason?
        var inst = $.jstree.reference('#dataTree');
        data = inst.get_selected(true);
        data.forEach(function(node){
            productListQuery.push(node.type + "=" + node.data.id);
        });

        var query = '{% url 'manage_action_containers' "add" "share" %}' + "?"+productListQuery.join("&");
        $("#create_share_form").attr("action", query)
        $("#create_share_form").load(query);
        // AJAX handling of create-discussion form
        $("#create_share_form").ajaxForm({
            success: function(html) {
                if (html.indexOf("shareId") > -1) {
                    var shareId = html.replace("shareId:", "");
                    $("#create_share_form").dialog( "close" );
                    $("#shareCreatedId").text(shareId);
                    $("#share_dialog_form").dialog("open").show();
                } else {
                    $("#create_share_form").html(html);
                }
            },
        });

        $("#share_dialog_form").dialog({
            autoOpen: false,
            resizable: true,
            height: 150,
            width:300,
            modal: true,
            buttons: {
                "OK": function() {
                    $( this ).dialog( "close" );
                }
            }
        });

            height: 600,
                "Accept": function() {
                    // simply submit the form
                    $("#create_share_form").submit();
                },
                "Cancel": function() {



<!-- hidden form for creating share - shown in dialog & loaded by AJAX -->
<form id="create_share_form" action="#" method="post" title="Create Share" class="standard_form">{% csrf_token %}
</form>

<form id="share_dialog_form" action="#" title="Create Share" style="display:none">
    <p style="font-size: 120%; font-weight: bold">
        Share <span id="shareCreatedId"></span> was created successfully.
</form>
            var writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1;
            var memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1;
                memberOfGroup = WEBCLIENT.eventContext.memberOfGroups.indexOf(WEBCLIENT.active_group_id) > -1,
                writeOwned = WEBCLIENT.eventContext.adminPrivileges.indexOf("WriteOwned") > -1,
    <div id="tree_details" class="left_panel_tree">
from .forms import ShareForm, BasketShareForm
    elif action == "add":
        template = "webclient/public/share_form.html"
        experimenters = list(conn.getExperimenters())
        experimenters.sort(key=lambda x: x.getOmeName().lower())
        if o_type == "share":
            img_ids = request.GET.getlist("image", request.POST.getlist("image"))
            if request.method == "GET" and len(img_ids) == 0:
                return HttpResponse("No images specified")
            images_to_share = list(conn.getObjects("Image", img_ids))
            if request.method == "POST":
                form = BasketShareForm(
                    initial={"experimenters": experimenters, "images": images_to_share},
                    data=request.POST.copy(),
                )
                if form.is_valid():
                    images = form.cleaned_data["image"]
                    message = form.cleaned_data["message"]
                    expiration = form.cleaned_data["expiration"]
                    members = form.cleaned_data["members"]
                    # guests = request.POST['guests']
                    enable = form.cleaned_data["enable"]
                    host = "%s?server=%i" % (
                        request.build_absolute_uri(
                            reverse("load_template", args=["public"])
                        ),
                        int(conn.server_id),
                    )
                    shareId = manager.createShare(
                        host, images, message, members, enable, expiration
                    )
                    return HttpResponse("shareId:%s" % shareId)
            else:
                initial = {
                    "experimenters": experimenters,
                    "images": images_to_share,
                    "enable": True,
                    "selected": request.GET.getlist("image"),
                }
                form = BasketShareForm(initial=initial)
        template = "webclient/public/share_form.html"
        context = {"manager": manager, "form": form}
                else None
        range_size = kwargs.get("limit", rows)
Fixed code: - Security vulnerability fixes for
  [2021-SV1](https://www.openmicroscopy.org/security/advisories/2021-SV1-user-context/),
  [2021-SV2](https://www.openmicroscopy.org/security/advisories/2021-SV2-url-validation/)

        context["html"] = context.get("html", {})
        context["html"]["meta_referrer"] = settings.HTML_META_REFERRER
    "omero.web.session_cookie_path": [
        "SESSION_COOKIE_PATH",
        None,
        leave_none_unset,
        "The path to use for session cookies",
    ],
    "omero.web.redirect_allowed_hosts": [
        "REDIRECT_ALLOWED_HOSTS",
        "[]",
        json.loads,
        (
            "If you wish to allow redirects to an external site, "
            "the domains must be listed here. "
            'For example ["openmicroscopy.org"].'
        ),
    ],
    "omero.web.html_meta_referrer": [
        "HTML_META_REFERRER",
        "origin-when-crossorigin",
        str,
        (
            "Default content for the HTML Meta referrer tag. "
            "See https://www.w3.org/TR/referrer-policy/#referrer-policies for "
            "allowed values and https://caniuse.com/#feat=referrer-policy for "
            "browser compatibility. "
            "Warning: Internet Explorer 11 does not support the default value "
            'of this setting, you may want to change this to "origin" after '
            "reviewing the linked documentation."
        ),
    ],
        super(render_response, self).prepare_context(request, context, *args, **kwargs)

                    memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1,
                    writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1,
        WEBCLIENT.member_of_groups = {{ member_of_groups|json_dumps|safe }};
  Copyright (C) 2011-2021 University of Dundee & Open Microscopy Environment.
            title: "Shares not supported",
            height: 250,
                "OK": function() {
<!-- hidden dialog -->
<div id="create_share_form" style="display:none">
    <p>Share functionality is no longer supported.</p>
    <p>Please see <a target="_blank" href="https://www.openmicroscopy.org/omero/features/share/">Sharing your data in OMERO</a>
        for alternative workflows.
</div>
            var writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1;
            var memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1;
                memberOfGroup = WEBCLIENT.member_of_groups.indexOf(WEBCLIENT.active_group_id) > -1,
                writeOwned = WEBCLIENT.current_admin_privileges.indexOf("WriteOwned") > -1,
    <div style="height: 110px; padding: 15px; box-sizing: border-box;">
        <p>Creating new shares is no longer supported. Previously created shares are shown below.</p>
        <p>Please see <a target="_blank" href="https://www.openmicroscopy.org/omero/features/share/">Sharing your data in
                OMERO</a>
            for alternative workflows.
        </p>
    </div>

    <div id="tree_details" class="left_panel_tree" style="height: calc(100% - 110px)">
from django.utils.http import is_safe_url
from .forms import ShareForm
def validate_redirect_url(url):
    """
    Returns a URL is safe to redirect to.
    If url is a different host, not in settings.REDIRECT_ALLOWED_HOSTS
    we return webclient index URL.
    """
    if not is_safe_url(url, allowed_hosts=settings.REDIRECT_ALLOWED_HOSTS):
        url = reverse("webindex")
    return url


        else:
            url = validate_redirect_url(url)
    url = validate_redirect_url(url)
    context["member_of_groups"] = conn.getEventContext().memberOfGroups
    <meta name="referrer" content="{{ html.meta_referrer|default:"origin-when-crossorigin" }}">
                else rows
        range_size = limit