CVE-2024-21642: Server-Side Request Forgery (SSRF) (CWE-918)
value: D-Tale is a visualizer for Pandas data structures. Users hosting versions D-Tale prior to 3.9.0 publicly can be vulnerable to server-side request forgery (SSRF), allowing attackers to access files on the server. Users should upgrade to version 3.9.0, where the `Load From the Web` input is turned off by default. The only workaround for versions earlier than 3.9.0 is to only host D-Tale to trusted users.
CWE 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.
CVSS v3 base score: 7.5
Published: 2024-01-05T22:15Z
Repository: https://github.com/man-group/dtale
Language: Python
Files changed: .circleci/config.yml, README.md, docs/CONFIGURATION.md, dtale/__init__.py, dtale/app.py, dtale/config.py, dtale/datasets.py, dtale/global_state.py, dtale/templates/dtale/base.html, dtale/views.py, frontend/static/__tests__/dtale/upload/Upload.test.support.tsx, frontend/static/__tests__/dtale/upload/upload-url-test.tsx, frontend/static/__tests__/reducers/dtale-test.tsx, frontend/static/__tests__/test-utils.tsx, frontend/static/popups/create/LabeledInput.tsx, frontend/static/popups/filter/FilterPopup.tsx, frontend/static/popups/upload/Upload.tsx, frontend/static/redux/reducers/app/settings.ts, frontend/static/redux/selectors.ts, frontend/static/redux/state/AppState.ts, requirements-test.txt, requirements.txt, tests/dtale/config/dtale.ini, tests/dtale/config/dtale_missing_props.ini, tests/dtale/config/test_config.py, tests/dtale/test_upload.py, tests/dtale/test_views.py
Commit date: 2024-01-05 15:03:54 -0500
Commit message: Updated web data uploads to be turned off by default

Diff: diff --git a/.circleci/config.yml b/.circleci/config.yml
index cb14ba67..24d076b2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -153,10 +153,11 @@ python: &python
           command: |
             virtualenv ci
             . ci/bin/activate
             # xlrd & xarray do not load correctly from "python setup.py develop"
             # possibly switch this script to use "pip install -r requirements.txt"
+            pip install pillow
             pip install xlrd
             pip install xarray
             if [ "${CIRCLE_JOB}" == "build_3_10" ]; then
               pip install pybind11
               pip install -r requirements.txt
@@ -189,10 +190,13 @@ python: &python
       - run:
           name: Setup tests
           command: |
             set -e
             . ci/bin/activate
+            if [ "${CIRCLE_JOB}" == "build_2_7" ]; then
+              pip install backports.functools-lru-cache==1.6.6
+            fi
             if [ "${CIRCLE_JOB}" != "build_2_7" ]; then
               pip install -e ".[arcticdb]"
             fi
             pip install -e ".[dash-bio]"
             pip install -e ".[redis]"
diff --git a/README.md b/README.md
index fdc686b3..ddb590c0 100644
--- a/README.md
+++ b/README.md
@@ -1350,10 +1350,23 @@ Here's the options at you disposal:
   * Movie dataset containing release date, director, actors, box office, reviews...
   * Video games and their sales
   * pandas.util.testing.makeTimeDataFrame
 
 
+**Starting with version 3.8.1 web uploads will be turned off by default.
+Web uploads are vulnerable to blind server side request forgery, please only use in trusted environments.**
+
+**You can turn this feature on by doing one of the following:**
+ - **add `enable_web_uploads=True` to your `dtale.show` call**
+ - **add `enable_web_uploads = False` to the [app] section of your dtale.ini config file ([more info](https://github.com/man-group/dtale/blob/master/docs/CONFIGURATION.md))**
+ - **run this code before calling dtale.show:**
+```python
+import dtale.global_state as global_state
+global_state.set_app_settings(dict(enable_web_uploads=True))
+```
+
+
 #### Instances
 This will give you information about other D-Tale instances are running under your current Python process.
 
 For example, if you ran the following script:
 ```python
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 6fbf4e1f..74a84d41 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -24,10 +24,11 @@ hide_header_editor = False
 lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
 enable_custom_filters = False
+enable_web_uploads = False
 
 [charts] # this controls how many points can be contained within scatter & 3D charts
 scatter_points = 15000
 3d_points = 4000
 
diff --git a/dtale/__init__.py b/dtale/__init__.py
index 3020b424..1ad57ef5 100644
--- a/dtale/__init__.py
+++ b/dtale/__init__.py
@@ -21,10 +21,11 @@ with warnings.catch_warnings():
     LOCK_HEADER_MENU = False
     HIDE_HEADER_MENU = False
     HIDE_MAIN_MENU = False
     HIDE_COLUMN_MENUS = False
     ENABLE_CUSTOM_FILTERS = False
+    ENABLE_WEB_UPLOADS = False
 
     # flake8: NOQA
     from dtale.app import show, get_instance, instances, offline_chart  # isort:skip
     from dtale.cli.loaders import LOADERS  # isort:skip
     from dtale.cli.clickutils import retrieve_meta_info_and_version
diff --git a/dtale/app.py b/dtale/app.py
index c3d4be5c..854ca884 100644
--- a/dtale/app.py
+++ b/dtale/app.py
@@ -719,10 +719,12 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
     :param highlight_filter: if True, then highlight rows on the frontend which will be filtered when applying a filter
                              rather than hiding them from the dataframe
     :type highlight_filter: boolean, optional
     :param enable_custom_filters: If true, this will enable users to make custom filters from the UI
     :type enable_custom_filters: bool, optional
+    :param enable_web_uploads: If true, this will enable users to upload files using URLs from the UI
+    :type enable_web_uploads: bool, optional
 
     :Example:
 
         >>> import dtale
         >>> import pandas as pd
@@ -805,10 +807,11 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
             lock_header_menu=final_options.get("lock_header_menu"),
             hide_header_menu=final_options.get("hide_header_menu"),
             hide_main_menu=final_options.get("hide_main_menu"),
             hide_column_menus=final_options.get("hide_column_menus"),
             enable_custom_filters=final_options.get("enable_custom_filters"),
+            enable_web_uploads=final_options.get("enable_web_uploads"),
         )
         instance.started_with_open_browser = final_options["open_browser"]
         is_active = not running_with_flask_debug() and is_up(app_url)
         if is_active:
 
diff --git a/dtale/config.py b/dtale/config.py
index 6e09727b..8cd97db8 100644
--- a/dtale/config.py
+++ b/dtale/config.py
@@ -112,10 +112,17 @@ def load_app_settings(config):
         curr_app_settings,
         "enable_custom_filters",
         section="app",
         getter="getboolean",
     )
+    enable_web_uploads = get_config_val(
+        config,
+        curr_app_settings,
+        "enable_web_uploads",
+        section="app",
+        getter="getboolean",
+    )
     open_custom_filter_on_startup = get_config_val(
         config,
         curr_app_settings,
         "open_custom_filter_on_startup",
         section="app",
@@ -151,10 +158,11 @@ def load_app_settings(config):
             lock_header_menu=lock_header_menu,
             hide_header_menu=hide_header_menu,
             hide_main_menu=hide_main_menu,
             hide_column_menus=hide_column_menus,
             enable_custom_filters=enable_custom_filters,
+            enable_web_uploads=enable_web_uploads,
         )
     )
 
 
 def load_chart_settings(config):
@@ -221,10 +229,11 @@ def build_show_options(options=None):
         lock_header_menu=None,
         hide_header_menu=None,
         hide_main_menu=None,
         hide_column_menus=None,
         enable_custom_filters=None,
+        enable_web_uploads=None,
     )
     config_options = {}
     config = get_config()
     if config and config.has_section("show"):
         config_options["host"] = get_config_val(config, defaults, "host")
diff --git a/dtale/datasets.py b/dtale/datasets.py
index 8176928d..c045bb93 100644
--- a/dtale/datasets.py
+++ b/dtale/datasets.py
@@ -1,8 +1,11 @@
+import numpy as np
 import pandas as pd
 import requests
+import string
 import zipfile
+from datetime import datetime
 
 from six import BytesIO
 
 
 def covid():
@@ -87,13 +90,21 @@ def movies():
     )
     return movies, None
 
 
 def time_dataframe():
-    try:
-        from pandas._testing import makeTimeDataFrame
-
-        return makeTimeDataFrame(), None
-    except ImportError:
-        from pandas.util.testing import makeTimeDataFrame
-
-        return makeTimeDataFrame(), None
+    def series_data():
+        if hasattr(np.random, "default_rng"):
+            return np.random.default_rng(2).standard_normal(30)
+        return np.random.randn(30)
+
+    cols = string.ascii_uppercase[:4]
+    data = {
+        c: pd.Series(
+            series_data(),
+            index=pd.DatetimeIndex(
+                pd.date_range(datetime(2000, 1, 1), periods=30, freq="B")
+            ),
+        )
+        for c in cols
+    }
+    return pd.DataFrame(data), None
diff --git a/dtale/global_state.py b/dtale/global_state.py
index 58684226..8a966a71 100644
--- a/dtale/global_state.py
+++ b/dtale/global_state.py
@@ -31,10 +31,11 @@ APP_SETTINGS = {
     "lock_header_menu": False,
     "hide_header_menu": False,
     "hide_main_menu": False,
     "hide_column_menus": False,
     "enable_custom_filters": False,
+    "enable_web_uploads": False,
 }
 
 AUTH_SETTINGS = {"active": False, "username": None, "password": None}
 
 CHART_SETTINGS = {"scatter_points": 15000, "3d_points": 40000}
@@ -614,10 +615,19 @@ def set_app_settings(settings):
                 (
                     "Turning on custom filtering. Custom filters are vulnerable to code injection attacks, please only "
                     "use in trusted environments."
                 )
             )
+    if settings.get("enable_web_uploads") is not None:
+        instance_updates["enable_web_uploads"] = settings.get("enable_web_uploads")
+        if instance_updates["enable_web_uploads"]:
+            logger.warning(
+                (
+                    "Turning on Web uploads. Web uploads are vulnerable to blind server side request forgery, please "
+                    "only use in trusted environments."
+                )
+            )
 
     if _default_store.size() > 0 and len(instance_updates):
         for data_id in _default_store.keys():
             update_settings(data_id, instance_updates)
 
diff --git a/dtale/templates/dtale/base.html b/dtale/templates/dtale/base.html
index d85d3f94..489514e1 100644
--- a/dtale/templates/dtale/base.html
+++ b/dtale/templates/dtale/base.html
@@ -43,10 +43,11 @@
         <input type="hidden" id="lock_header_menu" value="{{lock_header_menu}}" />
         <input type="hidden" id="hide_header_menu" value="{{hide_header_menu}}" />
         <input type="hidden" id="hide_main_menu" value="{{hide_main_menu}}" />
         <input type="hidden" id="hide_column_menus" value="{{hide_column_menus}}" />
         <input type="hidden" id="enable_custom_filters" value="{{enable_custom_filters}}" />
+        <input type="hidden" id="enable_web_uploads" value="{{enable_web_uploads}}" />
         <input type="hidden" id="allow_cell_edits" value="{{allow_cell_edits}}" />
         <input type="hidden" id="hide_drop_rows" value="{{hide_drop_rows}}" />
         <input type="hidden" id="is_vscode" value="{{is_vscode}}" />
         <input type="hidden" id="is_arcticdb" value="{{is_arcticdb}}" />
         <input type="hidden" id="arctic_conn" value="{{arctic_conn}}" />
diff --git a/dtale/views.py b/dtale/views.py
index 46ac2671..8139e5f8 100644
--- a/dtale/views.py
+++ b/dtale/views.py
@@ -350,10 +350,11 @@ class DtaleData(object):
                              hover over the top
         * hide_header_menu - if true, this will hide the header menu from the screen
         * hide_main_menu - if true, this will hide the main menu from the screen
         * hide_column_menus - if true, this will hide the column menus from the screen
         * enable_custom_filters - if True, allow users to specify custom filters from the UI using pandas.query strings
+        * enable_web_uploads - if True, allow users to upload files using URLs from the UI
 
         After applying please refresh any open browsers!
         """
         name_updates = dict(
             range_highlights="rangeHighlight",
@@ -914,10 +915,11 @@ def startup(
     lock_header_menu=None,
     hide_header_menu=None,
     hide_main_menu=None,
     hide_column_menus=None,
     enable_custom_filters=None,
+    enable_web_uploads=None,
     force_save=True,
 ):
     """
     Loads and stores data globally
      - If data has indexes then it will lock save those columns as locked on the front-end
@@ -1042,10 +1044,11 @@ def startup(
             lock_header_menu=lock_header_menu,
             hide_header_menu=hide_header_menu,
             hide_main_menu=hide_main_menu,
             hide_column_menus=hide_column_menus,
             enable_custom_filters=enable_custom_filters,
+            enable_web_uploads=enable_web_uploads,
         )
         startup_code = (
             "from arcticdb import Arctic\n"
             "from arcticdb.version_store._store import VersionedItem\n\n"
             "conn = Arctic('{uri}')\n"
@@ -1114,10 +1117,11 @@ def startup(
                 lock_header_menu=lock_header_menu,
                 hide_header_menu=hide_header_menu,
                 hide_main_menu=hide_main_menu,
                 hide_column_menus=hide_column_menus,
                 enable_custom_filters=enable_custom_filters,
+                enable_web_uploads=enable_web_uploads,
             )
 
             global_state.set_dataset(instance._data_id, data)
             global_state.set_dataset_dim(instance._data_id, {})
             return instance
@@ -1183,10 +1187,12 @@ def startup(
             base_settings["hide_main_menu"] = hide_main_menu
         if hide_column_menus is not None:
             base_settings["hide_column_menus"] = hide_column_menus
         if enable_custom_filters is not None:
             base_settings["enable_custom_filters"] = enable_custom_filters
+        if enable_web_uploads is not None:
+            base_settings["enable_web_uploads"] = enable_web_uploads
         if column_edit_options is not None:
             base_settings["column_edit_options"] = column_edit_options
         global_state.set_settings(data_id, base_settings)
         if optimize_dataframe and not global_state.is_arcticdb:
             data = optimize_df(data)
@@ -1237,10 +1243,17 @@ def startup(
                 (
                     "Custom filtering enabled. Custom filters are vulnerable to code injection attacks, please only "
                     "use in trusted environments."
                 )
             )
+        if global_state.load_flag(data_id, "enable_web_uploads", False):
+            logger.warning(
+                (
+                    "Web uploads enabled. Web uploads are vulnerable to blind server side request forgery, please "
+                    "only use in trusted environments."
+                )
+            )
         return DtaleData(data_id, url, is_proxy=is_proxy, app_root=app_root)
     else:
         raise NoDataLoadedException("No data has been loaded into this D-Tale session!")
 
 
@@ -1273,19 +1286,21 @@ def base_render_template(template, data_id, **kwargs):
     hide_main_menu = global_state.load_flag(data_id, "hide_main_menu", False)
     hide_column_menus = global_state.load_flag(data_id, "hide_column_menus", False)
     enable_custom_filters = global_state.load_flag(
         data_id, "enable_custom_filters", False
     )
+    enable_web_uploads = global_state.load_flag(data_id, "enable_web_uploads", False)
     app_overrides = dict(
         allow_cell_edits=json.dumps(allow_cell_edits),
         hide_shutdown=hide_shutdown,
         hide_header_editor=hide_header_editor,
         lock_header_menu=lock_header_menu,
         hide_header_menu=hide_header_menu,
         hide_main_menu=hide_main_menu,
         hide_column_menus=hide_column_menus,
         enable_custom_filters=enable_custom_filters,
+        enable_web_uploads=enable_web_uploads,
         github_fork=github_fork,
     )
     is_arcticdb = 0
     arctic_conn = ""
     if global_state.is_arcticdb:
@@ -3924,10 +3939,21 @@ def web_upload():
     from dtale.cli.loaders.csv_loader import loader_func as load_csv
     from dtale.cli.loaders.json_loader import loader_func as load_json
     from dtale.cli.loaders.excel_loader import load_file as load_excel
     from dtale.cli.loaders.parquet_loader import loader_func as load_parquet
 
+    if not global_state.get_app_settings().get("enable_web_uploads", False):
+        return jsonify(
+            dict(
+                success=False,
+                error=(
+                    "Web uploads not enabled! Web uploads are vulnerable to blind server side request forgery, please "
+                    "only use in trusted environments."
+                ),
+            )
+        )
+
     data_type = get_str_arg(request, "type")
     url = get_str_arg(request, "url")
     proxy = get_str_arg(request, "proxy")
     if data_type == "csv":
         df = load_csv(path=url, proxy=proxy)
diff --git a/frontend/static/__tests__/dtale/upload/Upload.test.support.tsx b/frontend/static/__tests__/dtale/upload/Upload.test.support.tsx
index f1ffdaf1..ef196baf 100644
--- a/frontend/static/__tests__/dtale/upload/Upload.test.support.tsx
+++ b/frontend/static/__tests__/dtale/upload/Upload.test.support.tsx
@@ -1,13 +1,16 @@
 import { act, render } from '@testing-library/react';
 import axios from 'axios';
 import * as React from 'react';
+import { Provider } from 'react-redux';
+import { Store } from 'redux';
 
 import Upload from '../../../popups/upload/Upload';
 import { Dataset, DataType } from '../../../popups/upload/UploadState';
 import * as UploadRepository from '../../../repository/UploadRepository';
 import reduxUtils from '../../redux-test-utils';
+import { buildInnerHTML } from '../../test-utils';
 
 /** Bundles alot of jest setup for CreateColumn component tests */
 export class Spies {
   public uploadSpy: jest.SpyInstance<Promise<UploadRepository.UploadResponse | undefined>, [params: FormData]>;
   public webUploadSpy: jest.SpyInstance<
@@ -15,18 +18,20 @@ export class Spies {
     [type: DataType, url: string, proxy?: string]
   >;
   public presetUploadSpy: jest.SpyInstance<Promise<UploadRepository.UploadResponse | undefined>, [dataset: Dataset]>;
   public readAsDataURLSpy: jest.SpyInstance;
   public btoaSpy: jest.SpyInstance;
+  public store: Store;
 
   /** Initializes all spy instances */
   constructor() {
     this.uploadSpy = jest.spyOn(UploadRepository, 'upload');
     this.webUploadSpy = jest.spyOn(UploadRepository, 'webUpload');
     this.presetUploadSpy = jest.spyOn(UploadRepository, 'presetUpload');
     this.readAsDataURLSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL');
     this.btoaSpy = jest.spyOn(window, 'btoa');
+    this.store = reduxUtils.createDtaleStore();
   }
 
   /** Sets the mockImplementation/mockReturnValue for spy instances */
   public setupMockImplementations(): void {
     (axios.get as any).mockImplementation(async (url: string) => Promise.resolve({ data: reduxUtils.urlFetcher(url) }));
@@ -46,14 +51,24 @@ export class Spies {
   }
 
   /**
    * Build the initial wrapper.
    *
+   * @param overrides redux overrides
    * @return the wrapper for testing.
    */
-  public async setupWrapper(): Promise<Element> {
+  public async setupWrapper(overrides?: Record<string, string>): Promise<Element> {
+    this.store = reduxUtils.createDtaleStore();
+    buildInnerHTML({ enableWebUploads: 'True', ...overrides }, this.store);
     return await act((): Element => {
-      const { container } = render(<Upload />);
+      const { container } = render(
+        <Provider store={this.store}>
+          <Upload />
+        </Provider>,
+        {
+          container: document.getElementById('content') ?? undefined,
+        },
+      );
       return container;
     });
   }
 }
diff --git a/frontend/static/__tests__/dtale/upload/upload-url-test.tsx b/frontend/static/__tests__/dtale/upload/upload-url-test.tsx
new file mode 100644
index 00000000..591f34c1
--- /dev/null
+++ b/frontend/static/__tests__/dtale/upload/upload-url-test.tsx
@@ -0,0 +1,48 @@
+import { screen } from '@testing-library/react';
+
+import { DISABLED_URL_UPLOADS_MSG } from '../../../popups/upload/Upload';
+
+import * as TestSupport from './Upload.test.support';
+
+describe('Upload', () => {
+  const { close, location, open, opener } = window;
+  const spies = new TestSupport.Spies();
+
+  beforeEach(async () => {
+    delete (window as any).location;
+    delete (window as any).close;
+    delete (window as any).open;
+    delete window.opener;
+    (window as any).location = {
+      reload: jest.fn(),
+      pathname: '/dtale/column/1',
+      href: '',
+      assign: jest.fn(),
+    };
+    window.close = jest.fn();
+    window.open = jest.fn();
+    window.opener = { location: { assign: jest.fn(), pathname: '/dtale/column/1' } };
+    spies.setupMockImplementations();
+    await spies.setupWrapper({ enableWebUploads: 'False' });
+  });
+
+  afterEach(() => spies.afterEach());
+
+  afterAll(() => {
+    spies.afterAll();
+    window.location = location;
+    window.close = close;
+    window.open = open;
+    window.opener = opener;
+  });
+
+  const upload = (): HTMLElement => screen.getByTestId('upload');
+
+  it('renders successfully', async () => {
+    expect(upload()).toBeDefined();
+  });
+
+  it('DataViewer: disabled web uploads', async () => {
+    expect(upload().getElementsByClassName('form-group')[0].textContent).toBe(DISABLED_URL_UPLOADS_MSG);
+  });
+});
diff --git a/frontend/static/__tests__/reducers/dtale-test.tsx b/frontend/static/__tests__/reducers/dtale-test.tsx
index 72f8cf9e..bf697bb7 100644
--- a/frontend/static/__tests__/reducers/dtale-test.tsx
+++ b/frontend/static/__tests__/reducers/dtale-test.tsx
@@ -27,10 +27,11 @@ describe('reducer tests', () => {
       lockHeaderMenu: false,
       hideHeaderMenu: false,
       hideMainMenu: false,
       hideColumnMenus: false,
       enableCustomFilters: false,
+      enableWebUploads: false,
       hideDropRows: false,
       iframe: false,
       columnMenuOpen: false,
       selectedCol: null,
       selectedColRef: null,
diff --git a/frontend/static/__tests__/test-utils.tsx b/frontend/static/__tests__/test-utils.tsx
index 518d5b78..a530b323 100644
--- a/frontend/static/__tests__/test-utils.tsx
+++ b/frontend/static/__tests__/test-utils.tsx
@@ -94,10 +94,11 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s
     buildHidden('lock_header_menu', props.lockHeaderMenu ?? HIDE_SHUTDOWN),
     buildHidden('hide_header_menu', props.hideHeaderMenu ?? HIDE_SHUTDOWN),
     buildHidden('hide_main_menu', props.hideMainMenu ?? HIDE_SHUTDOWN),
     buildHidden('hide_column_menus', props.hideColumnMenus ?? HIDE_SHUTDOWN),
     buildHidden('enable_custom_filters', props.enableCustomFilters ?? HIDE_SHUTDOWN),
+    buildHidden('enable_web_uploads', props.enableWebUploads ?? HIDE_SHUTDOWN),
     BASE_HTML,
   ].join('');
   store?.dispatch(actions.init());
 };
 
diff --git a/frontend/static/popups/create/LabeledInput.tsx b/frontend/static/popups/create/LabeledInput.tsx
index c78da8af..75c291c5 100644
--- a/frontend/static/popups/create/LabeledInput.tsx
+++ b/frontend/static/popups/create/LabeledInput.tsx
@@ -3,11 +3,11 @@ import * as React from 'react';
 /** Component properties for DtaleInput */
 interface DtaleInputProps {
   type?: React.HTMLInputTypeAttribute;
   value?: any;
   setter: (value: string) => void;
-  inputOptions?: Partial<React.HTMLAttributes<HTMLInputElement>>;
+  inputOptions?: Partial<React.AllHTMLAttributes<HTMLInputElement>>;
 }
 
 const DtaleInput: React.FC<DtaleInputProps> = ({ type = 'text', value, setter, inputOptions }) => (
   <input
     type={type}
diff --git a/frontend/static/popups/filter/FilterPopup.tsx b/frontend/static/popups/filter/FilterPopup.tsx
index 7009b30e..2effa067 100644
--- a/frontend/static/popups/filter/FilterPopup.tsx
+++ b/frontend/static/popups/filter/FilterPopup.tsx
@@ -26,11 +26,11 @@ export const DISABLED_CUSTOM_FILTERS_MSG = [
   'Custom Filtering is currently disabled.  This feature is only for trusted environments, in order to unlock this ',
   'feature you must do one of the following:\n\n',
   '- add "enable_custom_filters=True" to your dtale.show call\n',
   '- run this code before calling dtale.show\n',
   '\timport dtale.global_state as global_state\n\tglobal_state.set_app_settings(dict(enable_custom_filters=True))\n',
-  '- add "enable_custom_filters = False" to the [app] section of your dtale.ini config file',
+  '- add "enable_custom_filters = True" to the [app] section of your dtale.ini config file',
 ].join('');
 
 export const selectResult = createSelector(
   [selectDataId, selectQueryEngine, selectEnableCustomFilters, selectSettings],
   (dataId, queryEngine, enableCustomFilters, settings) => ({ dataId, queryEngine, enableCustomFilters, settings }),
diff --git a/frontend/static/popups/upload/Upload.tsx b/frontend/static/popups/upload/Upload.tsx
index a348eb50..d4b92908 100644
--- a/frontend/static/popups/upload/Upload.tsx
+++ b/frontend/static/popups/upload/Upload.tsx
@@ -1,12 +1,14 @@
 import * as React from 'react';
 import Dropzone from 'react-dropzone';
 import { withTranslation, WithTranslation } from 'react-i18next';
+import { useSelector } from 'react-redux';
 
 import { Bouncer } from '../../Bouncer';
 import { BouncerWrapper } from '../../BouncerWrapper';
 import ButtonToggle from '../../ButtonToggle';
+import { selectEnableWebUploads } from '../../redux/selectors';
 import { RemovableError } from '../../RemovableError';
 import * as UploadRepository from '../../repository/UploadRepository';
 import { LabeledInput } from '../create/LabeledInput';
 
 import CSVOptions from './CSVOptions';
@@ -23,16 +25,22 @@ const DATASET_LABELS: { [key in Dataset]: string } = {
   [Dataset.VIDEO_GAMES]: 'Video Games',
   [Dataset.MOVIES]: 'Movies',
   [Dataset.TIME_DATAFRAME]: 'makeTimeDataFrame',
 };
 
+export const DISABLED_URL_UPLOADS_MSG = [
+  'Web uploads are currently disabled.  This feature is only for trusted environments, in order to unlock this ',
+  'feature you must do one of the following:',
+].join('');
+
 /** Component properties for Upload */
 interface UploadProps {
   mergeRefresher?: () => Promise<void>;
 }
 
 const Upload: React.FC<UploadProps & WithTranslation> = ({ mergeRefresher, t }) => {
+  const enableWebUploads = useSelector(selectEnableWebUploads);
   const [dataType, setDataType] = React.useState<DataType>();
   const [url, setUrl] = React.useState<string>();
   const [proxy, setProxy] = React.useState<string>();
   const [loadingDataset, setLoadingDataset] = React.useState<Dataset>();
   const [datasetDescription, setDatasetDescription] = React.useState<string>();
@@ -154,31 +162,66 @@ const Upload: React.FC<UploadProps & WithTranslation> = ({ mergeRefresher, t })
               </button>
             </BouncerWrapper>
           )}
         </div>
       </div>
-      <div className="form-group row">
-        <label className="col-md-3 col-form-label text-right">{t('Data Type')}</label>
-        <div className="col-md-8 p-0">
-          <ButtonToggle
-            options={Object.values(DataType).map((value) => ({ value, label: value.toUpperCase() }))}
-            defaultValue={dataType}
-            update={setDataType}
-          />
+      {!enableWebUploads && (
+        <div className="form-group row">
+          <div className="col-md-2 p-0" />
+          <div className="col-md-8 col-form-label text-left">
+            <label>{DISABLED_URL_UPLOADS_MSG}</label>
+            <ul>
+              <li>
+                {`add `}
+                <code>enable_web_uploads=True</code>
+                {` to your `}
+                <code className="font-weight-bold">dtale.show</code>
+                {` call`}
+              </li>
+              <li>
+                {`run this code before calling `}
+                <code className="font-weight-bold">dtale.show</code>
+                <pre>
+                  {`import dtale.global_state as global_state\n`}
+                  {`global_state.set_app_settings(dict(enable_web_uploads=True))`}
+                </pre>
+              </li>
+              <li>
+                {`add `}
+                <code>enable_web_uploads = True</code>
+                {` to the [app] section of your dtale.ini config file`}
+              </li>
+            </ul>
+          </div>
+          <div className="col-md-2 p-0" />
         </div>
-      </div>
-      <LabeledInput label="URL" value={url} setter={setUrl} />
-      <LabeledInput
-        label={
-          <React.Fragment>
-            {t('Proxy')}
-            <small className="pl-3">{t('(Optional)')}</small>
-          </React.Fragment>
-        }
-        value={proxy}
-        setter={setProxy}
-      />
+      )}
+      {enableWebUploads && (
+        <>
+          <div className="form-group row">
+            <label className="col-md-3 col-form-label text-right">{t('Data Type')}</label>
+            <div className="col-md-8 p-0">
+              <ButtonToggle
+                options={Object.values(DataType).map((value) => ({ value, label: value.toUpperCase() }))}
+                defaultValue={dataType}
+                update={setDataType}
+              />
+            </div>
+          </div>
+          <LabeledInput label="URL" value={url} setter={setUrl} />
+          <LabeledInput
+            label={
+              <React.Fragment>
+                {t('Proxy')}
+                <small className="pl-3">{t('(Optional)')}</small>
+              </React.Fragment>
+            }
+            value={proxy}
+            setter={setProxy}
+          />
+        </>
+      )}
       <div className="pb-5">
         <h3 className="d-inline">{t('Sample Datasets')}</h3>
         <small className="pl-3 d-inline">{t('(Requires access to web)')}</small>
       </div>
       <div className="form-group row pl-5 pr-5">
diff --git a/frontend/static/redux/reducers/app/settings.ts b/frontend/static/redux/reducers/app/settings.ts
index 4a01d118..98669a89 100644
--- a/frontend/static/redux/reducers/app/settings.ts
+++ b/frontend/static/redux/reducers/app/settings.ts
@@ -91,10 +91,21 @@ export const enableCustomFilters = (state = false, action: AppActionTypes): bool
     default:
       return state;
   }
 };
 
+export const enableWebUploads = (state = false, action: AppActionTypes): boolean => {
+  switch (action.type) {
+    case ActionType.INIT_PARAMS:
+      return toBool(getHiddenValue('enable_web_uploads'));
+    case ActionType.LOAD_PREVIEW:
+      return false;
+    default:
+      return state;
+  }
+};
+
 export const openCustomFilterOnStartup = (state = false, action: AppActionTypes): boolean => {
   switch (action.type) {
     case ActionType.INIT_PARAMS:
       return toBool(getHiddenValue('open_custom_filter_on_startup'));
     case ActionType.LOAD_PREVIEW:
diff --git a/frontend/static/redux/selectors.ts b/frontend/static/redux/selectors.ts
index 497eb755..7b302c24 100644
--- a/frontend/static/redux/selectors.ts
+++ b/frontend/static/redux/selectors.ts
@@ -57,10 +57,11 @@ export const selectRangeHighlight = createSelector([selectSettings], (settings)
 export const selectBaseLockHeaderMenu = (state: AppState): boolean => state.lockHeaderMenu;
 export const selectBaseHideHeaderMenu = (state: AppState): boolean => state.hideHeaderMenu;
 export const selectBaseHideMainMenu = (state: AppState): boolean => state.hideMainMenu;
 export const selectBaseHideColumnMenus = (state: AppState): boolean => state.hideColumnMenus;
 export const selectBaseEnableCustomFilters = (state: AppState): boolean => state.enableCustomFilters;
+export const selectEnableWebUploads = (state: AppState): boolean => state.enableWebUploads;
 export const selectFilteredRanges = (state: AppState): FilteredRanges => state.filteredRanges;
 export const selectShowAllHeatmapColumns = (state: AppState): boolean => state.showAllHeatmapColumns;
 export const selectChartData = (state: AppState): Popups => state.chartData;
 export const selectSidePanel = (state: AppState): SidePanelProps => state.sidePanel;
 export const selectSidePanelVisible = createSelector([selectSidePanel], (sidePanel) => sidePanel.visible);
diff --git a/frontend/static/redux/state/AppState.ts b/frontend/static/redux/state/AppState.ts
index 8f10e445..816b0543 100644
--- a/frontend/static/redux/state/AppState.ts
+++ b/frontend/static/redux/state/AppState.ts
@@ -438,10 +438,11 @@ export interface AppSettings {
   lockHeaderMenu: boolean;
   hideHeaderMenu: boolean;
   hideMainMenu: boolean;
   hideColumnMenus: boolean;
   enableCustomFilters: boolean;
+  enableWebUploads: boolean;
 }
 
 /** Properties for specifying filtered ranges */
 export interface FilteredRanges {
   query?: string;
diff --git a/requirements-test.txt b/requirements-test.txt
index dd484c0a..188e453d 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -3,6 +3,7 @@ importlib-metadata; python_version != '3.6'
 ipython<8.0.0
 mock
 nbconvert
 pytest
 pytest-cov
-pytest-server-fixtures
+pytest-server-fixtures<=1.7.0; python_version == '2.7'
+pytest-server-fixtures; python_version >= '3.6'
diff --git a/requirements.txt b/requirements.txt
index 5b09d666..a9538563 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -79,22 +79,23 @@ pyparsing<=2.4.7; python_version == '2.7'
 requests; python_version >= '3.7'
 requests<=2.27.1; python_version <= '3.6'
 scikit-learn<=0.20.4; python_version < '3.0'
 scikit-learn<=0.24.2; python_version == '3.6'
 scikit-learn<=1.0.2; python_version == '3.7'
-scikit-learn; python_version > '3.7'
+scikit-learn<=1.3.2; python_version == '3.8'
+scikit-learn; python_version > '3.8'
 scipy<=1.2.3; python_version == '2.7'
 scipy<=1.5.4; python_version == '3.0'
 scipy<=1.5.4; python_version == '3.1'
 scipy<=1.5.4; python_version == '3.2'
 scipy<=1.5.4; python_version == '3.3'
 scipy<=1.5.4; python_version == '3.4'
 scipy<=1.5.4; python_version == '3.5'
 scipy<=1.5.4; python_version == '3.6'
 scipy<=1.7.3; python_version == '3.7'
 scipy<=1.10.1; python_version == '3.8'
-scipy; python_version >= '3.9'
+scipy!=1.12.0rc1; python_version >= '3.9'
 seaborn<=0.9.1; python_version < '3.6'
 seaborn<=0.11.2; python_version == '3.6'
 seaborn<=0.12.2; python_version == '3.7'
 seaborn; python_version > '3.7'
 squarify
diff --git a/tests/dtale/config/dtale.ini b/tests/dtale/config/dtale.ini
index 52fa121f..9d1e4da3 100644
--- a/tests/dtale/config/dtale.ini
+++ b/tests/dtale/config/dtale.ini
@@ -12,10 +12,11 @@ hide_header_editor = False
 lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
 enable_custom_filters = False
+enable_web_uploads = False
 
 [charts]
 scatter_points = 15000
 3d_points = 40000
 
diff --git a/tests/dtale/config/dtale_missing_props.ini b/tests/dtale/config/dtale_missing_props.ini
index 9d2c9343..e7ab1ad2 100644
--- a/tests/dtale/config/dtale_missing_props.ini
+++ b/tests/dtale/config/dtale_missing_props.ini
@@ -4,5 +4,6 @@ hide_header_editor = False
 lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
 enable_custom_filters = False
+enable_web_uploads = False
diff --git a/tests/dtale/config/test_config.py b/tests/dtale/config/test_config.py
index e8c04970..da099aac 100644
--- a/tests/dtale/config/test_config.py
+++ b/tests/dtale/config/test_config.py
@@ -26,10 +26,11 @@ def test_load_app_settings():
         "lock_header_menu": True,
         "hide_header_menu": True,
         "hide_main_menu": True,
         "hide_column_menus": True,
         "enable_custom_filters": True,
+        "enable_web_uploads": True,
     }
     with ExitStack() as stack:
         stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings))
 
         load_app_settings(None)
@@ -43,10 +44,11 @@ def test_load_app_settings():
         assert settings["lock_header_menu"]
         assert settings["hide_header_menu"]
         assert settings["hide_main_menu"]
         assert settings["hide_column_menus"]
         assert settings["enable_custom_filters"]
+        assert settings["enable_web_uploads"]
 
         load_app_settings(
             load_config_state(os.path.join(os.path.dirname(__file__), "dtale.ini"))
         )
         assert not settings["hide_shutdown"]
@@ -61,10 +63,11 @@ def test_load_app_settings():
         assert not settings["lock_header_menu"]
         assert not settings["hide_header_menu"]
         assert not settings["hide_main_menu"]
         assert not settings["hide_column_menus"]
         assert not settings["enable_custom_filters"]
+        assert not settings["enable_web_uploads"]
 
 
 @pytest.mark.unit
 def test_load_app_settings_w_missing_props():
     settings = {
@@ -79,10 +82,11 @@ def test_load_app_settings_w_missing_props():
         "lock_header_menu": True,
         "hide_header_menu": True,
         "hide_main_menu": True,
         "hide_column_menus": True,
         "enable_custom_filters": True,
+        "enable_web_uploads": True,
     }
     with ExitStack() as stack:
         stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings))
 
         load_app_settings(None)
@@ -94,10 +98,11 @@ def test_load_app_settings_w_missing_props():
         assert settings["lock_header_menu"]
         assert settings["hide_header_menu"]
         assert settings["hide_main_menu"]
         assert settings["hide_column_menus"]
         assert settings["enable_custom_filters"]
+        assert settings["enable_web_uploads"]
 
         load_app_settings(
             load_config_state(
                 os.path.join(os.path.dirname(__file__), "dtale_missing_props.ini")
             )
@@ -110,10 +115,11 @@ def test_load_app_settings_w_missing_props():
         assert not settings["lock_header_menu"]
         assert not settings["hide_header_menu"]
         assert not settings["hide_main_menu"]
         assert not settings["hide_column_menus"]
         assert not settings["enable_custom_filters"]
+        assert not settings["enable_web_uploads"]
 
 
 @pytest.mark.unit
 def test_load_auth_settings():
     settings = {"active": True, "username": "foo", "password": "foo"}
diff --git a/tests/dtale/test_upload.py b/tests/dtale/test_upload.py
index ec28265c..fb055160 100644
--- a/tests/dtale/test_upload.py
+++ b/tests/dtale/test_upload.py
@@ -126,10 +126,11 @@ def test_upload(unittest):
 @pytest.mark.unit
 def test_web_upload(unittest):
     import dtale.global_state as global_state
 
     global_state.clear_store()
+    global_state.set_app_settings(dict(enable_web_uploads=True))
     with build_app(url=URL).test_client() as c:
         with ExitStack() as stack:
             load_csv = stack.enter_context(
                 mock.patch(
                     "dtale.cli.loaders.csv_loader.loader_func",
@@ -199,10 +200,20 @@ def test_web_upload(unittest):
             assert len(sheets) == 2
             unittest.assertEqual(
                 sorted([s["name"] for s in sheets]), ["Sheet 1", "Sheet 2"]
             )
 
+            global_state.set_app_settings(dict(enable_web_uploads=False))
+            resp = c.get("/dtale/web-upload", query_string=params)
+            response_data = resp.get_json()
+            assert not response_data["success"]
+            assert response_data["error"] == (
+                "Web uploads not enabled! Web uploads are vulnerable to blind server side request forgery, please "
+                "only use in trusted environments."
+            )
+            global_state.set_app_settings(dict(enable_web_uploads=True))
+
 
 @pytest.mark.unit
 def test_covid_dataset():
     import dtale.global_state as global_state
 
diff --git a/tests/dtale/test_views.py b/tests/dtale/test_views.py
index fc47f7cf..f2bd0168 100644
--- a/tests/dtale/test_views.py
+++ b/tests/dtale/test_views.py
@@ -79,10 +79,11 @@ def test_startup(unittest):
         lock_header_menu=True,
         hide_header_menu=True,
         hide_main_menu=True,
         hide_column_menus=True,
         enable_custom_filters=True,
+        enable_web_uploads=True,
     )
 
     pdt.assert_frame_equal(instance.data, test_data.reset_index())
     unittest.assertEqual(
         global_state.get_settings(instance._data_id),
@@ -94,10 +95,11 @@ def test_startup(unittest):
             lock_header_menu=True,
             hide_header_menu=True,
             hide_main_menu=True,
             hide_column_menus=True,
             enable_custom_filters=True,
+            enable_web_uploads=True,
             locked=["date", "security_id"],
             indexes=["date", "security_id"],
             precision=2,
             sortInfo=[("security_id", "ASC")],
             rangeHighlight=None,
@@ -119,10 +121,11 @@ def test_startup(unittest):
             lock_header_menu=True,
             hide_header_menu=True,
             hide_main_menu=True,
             hide_column_menus=True,
             enable_custom_filters=True,
+            enable_web_uploads=True,
             locked=["date", "security_id"],
             indexes=["date", "security_id"],
             precision=2,
             sortInfo=[("security_id", "ASC")],
             rangeHighlight=None,

Vulnerable code:     try:
        from pandas._testing import makeTimeDataFrame

        return makeTimeDataFrame(), None
    except ImportError:
        from pandas.util.testing import makeTimeDataFrame

        return makeTimeDataFrame(), None
  public async setupWrapper(): Promise<Element> {
      const { container } = render(<Upload />);
  inputOptions?: Partial<React.HTMLAttributes<HTMLInputElement>>;
  '- add "enable_custom_filters = False" to the [app] section of your dtale.ini config file',
      <div className="form-group row">
        <label className="col-md-3 col-form-label text-right">{t('Data Type')}</label>
        <div className="col-md-8 p-0">
          <ButtonToggle
            options={Object.values(DataType).map((value) => ({ value, label: value.toUpperCase() }))}
            defaultValue={dataType}
            update={setDataType}
          />
      </div>
      <LabeledInput label="URL" value={url} setter={setUrl} />
      <LabeledInput
        label={
          <React.Fragment>
            {t('Proxy')}
            <small className="pl-3">{t('(Optional)')}</small>
          </React.Fragment>
        }
        value={proxy}
        setter={setProxy}
      />
pytest-server-fixtures
scikit-learn; python_version > '3.7'
scipy; python_version >= '3.9'
Fixed code:             pip install pillow
            if [ "${CIRCLE_JOB}" == "build_2_7" ]; then
              pip install backports.functools-lru-cache==1.6.6
            fi
**Starting with version 3.8.1 web uploads will be turned off by default.
Web uploads are vulnerable to blind server side request forgery, please only use in trusted environments.**

**You can turn this feature on by doing one of the following:**
 - **add `enable_web_uploads=True` to your `dtale.show` call**
 - **add `enable_web_uploads = False` to the [app] section of your dtale.ini config file ([more info](https://github.com/man-group/dtale/blob/master/docs/CONFIGURATION.md))**
 - **run this code before calling dtale.show:**
```python
import dtale.global_state as global_state
global_state.set_app_settings(dict(enable_web_uploads=True))
```


enable_web_uploads = False
    ENABLE_WEB_UPLOADS = False
    :param enable_web_uploads: If true, this will enable users to upload files using URLs from the UI
    :type enable_web_uploads: bool, optional
            enable_web_uploads=final_options.get("enable_web_uploads"),
    enable_web_uploads = get_config_val(
        config,
        curr_app_settings,
        "enable_web_uploads",
        section="app",
        getter="getboolean",
    )
            enable_web_uploads=enable_web_uploads,
        enable_web_uploads=None,
import numpy as np
import string
from datetime import datetime
    def series_data():
        if hasattr(np.random, "default_rng"):
            return np.random.default_rng(2).standard_normal(30)
        return np.random.randn(30)

    cols = string.ascii_uppercase[:4]
    data = {
        c: pd.Series(
            series_data(),
            index=pd.DatetimeIndex(
                pd.date_range(datetime(2000, 1, 1), periods=30, freq="B")
            ),
        )
        for c in cols
    }
    return pd.DataFrame(data), None
    "enable_web_uploads": False,
    if settings.get("enable_web_uploads") is not None:
        instance_updates["enable_web_uploads"] = settings.get("enable_web_uploads")
        if instance_updates["enable_web_uploads"]:
            logger.warning(
                (
                    "Turning on Web uploads. Web uploads are vulnerable to blind server side request forgery, please "
                    "only use in trusted environments."
                )
            )
        <input type="hidden" id="enable_web_uploads" value="{{enable_web_uploads}}" />
        * enable_web_uploads - if True, allow users to upload files using URLs from the UI
    enable_web_uploads=None,
            enable_web_uploads=enable_web_uploads,
                enable_web_uploads=enable_web_uploads,
        if enable_web_uploads is not None:
            base_settings["enable_web_uploads"] = enable_web_uploads
        if global_state.load_flag(data_id, "enable_web_uploads", False):
            logger.warning(
                (
                    "Web uploads enabled. Web uploads are vulnerable to blind server side request forgery, please "
                    "only use in trusted environments."
                )
            )
    enable_web_uploads = global_state.load_flag(data_id, "enable_web_uploads", False)
        enable_web_uploads=enable_web_uploads,
    if not global_state.get_app_settings().get("enable_web_uploads", False):
        return jsonify(
            dict(
                success=False,
                error=(
                    "Web uploads not enabled! Web uploads are vulnerable to blind server side request forgery, please "
                    "only use in trusted environments."
                ),
            )
        )

import { Provider } from 'react-redux';
import { Store } from 'redux';
import { buildInnerHTML } from '../../test-utils';
  public store: Store;
    this.store = reduxUtils.createDtaleStore();
   * @param overrides redux overrides
  public async setupWrapper(overrides?: Record<string, string>): Promise<Element> {
    this.store = reduxUtils.createDtaleStore();
    buildInnerHTML({ enableWebUploads: 'True', ...overrides }, this.store);
      const { container } = render(
        <Provider store={this.store}>
          <Upload />
        </Provider>,
        {
          container: document.getElementById('content') ?? undefined,
        },
      );
import { screen } from '@testing-library/react';

import { DISABLED_URL_UPLOADS_MSG } from '../../../popups/upload/Upload';

import * as TestSupport from './Upload.test.support';

describe('Upload', () => {
  const { close, location, open, opener } = window;
  const spies = new TestSupport.Spies();

  beforeEach(async () => {
    delete (window as any).location;
    delete (window as any).close;
    delete (window as any).open;
    delete window.opener;
    (window as any).location = {
      reload: jest.fn(),
      pathname: '/dtale/column/1',
      href: '',
      assign: jest.fn(),
    };
    window.close = jest.fn();
    window.open = jest.fn();
    window.opener = { location: { assign: jest.fn(), pathname: '/dtale/column/1' } };
    spies.setupMockImplementations();
    await spies.setupWrapper({ enableWebUploads: 'False' });
  });

  afterEach(() => spies.afterEach());

  afterAll(() => {
    spies.afterAll();
    window.location = location;
    window.close = close;
    window.open = open;
    window.opener = opener;
  });

  const upload = (): HTMLElement => screen.getByTestId('upload');

  it('renders successfully', async () => {
    expect(upload()).toBeDefined();
  });

  it('DataViewer: disabled web uploads', async () => {
    expect(upload().getElementsByClassName('form-group')[0].textContent).toBe(DISABLED_URL_UPLOADS_MSG);
  });
});
      enableWebUploads: false,
    buildHidden('enable_web_uploads', props.enableWebUploads ?? HIDE_SHUTDOWN),
  inputOptions?: Partial<React.AllHTMLAttributes<HTMLInputElement>>;
  '- add "enable_custom_filters = True" to the [app] section of your dtale.ini config file',
import { useSelector } from 'react-redux';
import { selectEnableWebUploads } from '../../redux/selectors';
export const DISABLED_URL_UPLOADS_MSG = [
  'Web uploads are currently disabled.  This feature is only for trusted environments, in order to unlock this ',
  'feature you must do one of the following:',
].join('');

  const enableWebUploads = useSelector(selectEnableWebUploads);
      {!enableWebUploads && (
        <div className="form-group row">
          <div className="col-md-2 p-0" />
          <div className="col-md-8 col-form-label text-left">
            <label>{DISABLED_URL_UPLOADS_MSG}</label>
            <ul>
              <li>
                {`add `}
                <code>enable_web_uploads=True</code>
                {` to your `}
                <code className="font-weight-bold">dtale.show</code>
                {` call`}
              </li>
              <li>
                {`run this code before calling `}
                <code className="font-weight-bold">dtale.show</code>
                <pre>
                  {`import dtale.global_state as global_state\n`}
                  {`global_state.set_app_settings(dict(enable_web_uploads=True))`}
                </pre>
              </li>
              <li>
                {`add `}
                <code>enable_web_uploads = True</code>
                {` to the [app] section of your dtale.ini config file`}
              </li>
            </ul>
          </div>
          <div className="col-md-2 p-0" />
      )}
      {enableWebUploads && (
        <>
          <div className="form-group row">
            <label className="col-md-3 col-form-label text-right">{t('Data Type')}</label>
            <div className="col-md-8 p-0">
              <ButtonToggle
                options={Object.values(DataType).map((value) => ({ value, label: value.toUpperCase() }))}
                defaultValue={dataType}
                update={setDataType}
              />
            </div>
          </div>
          <LabeledInput label="URL" value={url} setter={setUrl} />
          <LabeledInput
            label={
              <React.Fragment>
                {t('Proxy')}
                <small className="pl-3">{t('(Optional)')}</small>
              </React.Fragment>
            }
            value={proxy}
            setter={setProxy}
          />
        </>
      )}
export const enableWebUploads = (state = false, action: AppActionTypes): boolean => {
  switch (action.type) {
    case ActionType.INIT_PARAMS:
      return toBool(getHiddenValue('enable_web_uploads'));
    case ActionType.LOAD_PREVIEW:
      return false;
    default:
      return state;
  }
};

export const selectEnableWebUploads = (state: AppState): boolean => state.enableWebUploads;
  enableWebUploads: boolean;
pytest-server-fixtures<=1.7.0; python_version == '2.7'
pytest-server-fixtures; python_version >= '3.6'
scikit-learn<=1.3.2; python_version == '3.8'
scikit-learn; python_version > '3.8'
scipy!=1.12.0rc1; python_version >= '3.9'
enable_web_uploads = False
enable_web_uploads = False
        "enable_web_uploads": True,
        assert settings["enable_web_uploads"]
        assert not settings["enable_web_uploads"]
        "enable_web_uploads": True,
        assert settings["enable_web_uploads"]
        assert not settings["enable_web_uploads"]
    global_state.set_app_settings(dict(enable_web_uploads=True))
            global_state.set_app_settings(dict(enable_web_uploads=False))
            resp = c.get("/dtale/web-upload", query_string=params)
            response_data = resp.get_json()
            assert not response_data["success"]
            assert response_data["error"] == (
                "Web uploads not enabled! Web uploads are vulnerable to blind server side request forgery, please "
                "only use in trusted environments."
            )
            global_state.set_app_settings(dict(enable_web_uploads=True))

        enable_web_uploads=True,
            enable_web_uploads=True,
            enable_web_uploads=True,