D-Tale server-side request forgery through Web uploads
Description
D-Tale prior to 3.9.0 has an SSRF vulnerability via the 'Load From the Web' feature, allowing attackers to access internal files.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
D-Tale prior to 3.9.0 has an SSRF vulnerability via the 'Load From the Web' feature, allowing attackers to access internal files.
Vulnerability
Overview
CVE-2024-21642 describes a server-side request forgery (SSRF) vulnerability in D-Tale, a web-based visualizer for Pandas data structures. The issue resides in the "Load From the Web" functionality, which allowed users to upload data files from arbitrary URLs. In versions prior to 3.9.0, this feature was enabled by default, and without proper validation, an attacker could force the server to make requests to internal or external resources, leading to unauthorized access to files on the server [1][3].
Attack
Vector and Prerequisites
An attacker can exploit this by sending a crafted request to the D-Tale server with a URL pointing to an internal system (e.g., file:///etc/passwd or an internal service). Since the feature is part of the standard UI and does not require authentication in public deployments, any user who can interact with the web interface can trigger the SSRF. The vulnerability is particularly dangerous when D-Tale is hosted openly on the internet [3].
Impact
Successful exploitation allows the attacker to read arbitrary files from the server's file system, including sensitive configuration files, application secrets, or other data. This could lead to further compromise of the host or adjacent internal networks [3].
Mitigation
The vendor has addressed this by releasing D-Tale version 3.9.0, where the "Load From the Web" input is turned off by default. Users can still enable it explicitly via the enable_web_uploads parameter if needed [4]. For versions earlier than 3.9.0, the only workaround is to restrict access to only trusted users [3]. Upgrading to 3.9.0 or later is strongly recommended.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
dtalePyPI | < 3.9.0 | 3.9.0 |
Affected products
2Patches
1954f6be1a06fUpdated web data uploads to be turned off by default
27 files changed · +259 −35
.circleci/config.yml+4 −0 modified@@ -155,6 +155,7 @@ python: &python . 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 @@ -191,6 +192,9 @@ python: &python 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
docs/CONFIGURATION.md+1 −0 modified@@ -26,6 +26,7 @@ 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
dtale/app.py+3 −0 modified@@ -721,6 +721,8 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options): :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: @@ -807,6 +809,7 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options): 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)
dtale/config.py+9 −0 modified@@ -114,6 +114,13 @@ def load_app_settings(config): 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, @@ -153,6 +160,7 @@ def load_app_settings(config): hide_main_menu=hide_main_menu, hide_column_menus=hide_column_menus, enable_custom_filters=enable_custom_filters, + enable_web_uploads=enable_web_uploads, ) ) @@ -223,6 +231,7 @@ def build_show_options(options=None): hide_main_menu=None, hide_column_menus=None, enable_custom_filters=None, + enable_web_uploads=None, ) config_options = {} config = get_config()
dtale/datasets.py+19 −8 modified@@ -1,6 +1,9 @@ +import numpy as np import pandas as pd import requests +import string import zipfile +from datetime import datetime from six import BytesIO @@ -89,11 +92,19 @@ def movies(): 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
dtale/global_state.py+10 −0 modified@@ -33,6 +33,7 @@ "hide_main_menu": False, "hide_column_menus": False, "enable_custom_filters": False, + "enable_web_uploads": False, } AUTH_SETTINGS = {"active": False, "username": None, "password": None} @@ -616,6 +617,15 @@ def set_app_settings(settings): "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():
dtale/__init__.py+1 −0 modified@@ -23,6 +23,7 @@ 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
dtale/templates/dtale/base.html+1 −0 modified@@ -45,6 +45,7 @@ <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}}" />
dtale/views.py+26 −0 modified@@ -352,6 +352,7 @@ def update_settings(self, **updates): * 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! """ @@ -916,6 +917,7 @@ def startup( hide_main_menu=None, hide_column_menus=None, enable_custom_filters=None, + enable_web_uploads=None, force_save=True, ): """ @@ -1044,6 +1046,7 @@ def startup( 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" @@ -1116,6 +1119,7 @@ def startup( 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) @@ -1185,6 +1189,8 @@ def startup( 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) @@ -1239,6 +1245,13 @@ def startup( "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!") @@ -1275,6 +1288,7 @@ def base_render_template(template, data_id, **kwargs): 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, @@ -1284,6 +1298,7 @@ def base_render_template(template, data_id, **kwargs): 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 @@ -3926,6 +3941,17 @@ def web_upload(): 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")
frontend/static/popups/create/LabeledInput.tsx+1 −1 modified@@ -5,7 +5,7 @@ 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 }) => (
frontend/static/popups/filter/FilterPopup.tsx+1 −1 modified@@ -28,7 +28,7 @@ export const DISABLED_CUSTOM_FILTERS_MSG = [ '- 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(
frontend/static/popups/upload/Upload.tsx+63 −20 modified@@ -1,10 +1,12 @@ 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'; @@ -25,12 +27,18 @@ const DATASET_LABELS: { [key in Dataset]: string } = { [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>(); @@ -156,27 +164,62 @@ const Upload: React.FC<UploadProps & WithTranslation> = ({ mergeRefresher, t }) )} </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>
frontend/static/redux/reducers/app/settings.ts+11 −0 modified@@ -93,6 +93,17 @@ export const enableCustomFilters = (state = false, action: AppActionTypes): bool } }; +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:
frontend/static/redux/selectors.ts+1 −0 modified@@ -59,6 +59,7 @@ export const selectBaseHideHeaderMenu = (state: AppState): boolean => state.hide 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;
frontend/static/redux/state/AppState.ts+1 −0 modified@@ -440,6 +440,7 @@ export interface AppSettings { hideMainMenu: boolean; hideColumnMenus: boolean; enableCustomFilters: boolean; + enableWebUploads: boolean; } /** Properties for specifying filtered ranges */
frontend/static/__tests__/dtale/upload/Upload.test.support.tsx+17 −2 modified@@ -1,11 +1,14 @@ 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 { @@ -17,6 +20,7 @@ export class Spies { 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() { @@ -25,6 +29,7 @@ export class Spies { 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 */ @@ -48,11 +53,21 @@ 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; }); }
frontend/static/__tests__/dtale/upload/upload-url-test.tsx+48 −0 added@@ -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); + }); +});
frontend/static/__tests__/reducers/dtale-test.tsx+1 −0 modified@@ -29,6 +29,7 @@ describe('reducer tests', () => { hideMainMenu: false, hideColumnMenus: false, enableCustomFilters: false, + enableWebUploads: false, hideDropRows: false, iframe: false, columnMenuOpen: false,
frontend/static/__tests__/test-utils.tsx+1 −0 modified@@ -96,6 +96,7 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s 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());
README.md+13 −0 modified@@ -1352,6 +1352,19 @@ Here's the options at you disposal: * 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.
requirements-test.txt+2 −1 modified@@ -5,4 +5,5 @@ 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'
requirements.txt+3 −2 modified@@ -81,7 +81,8 @@ 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' @@ -92,7 +93,7 @@ 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'
tests/dtale/config/dtale.ini+1 −0 modified@@ -14,6 +14,7 @@ hide_header_menu = False hide_main_menu = False hide_column_menus = False enable_custom_filters = False +enable_web_uploads = False [charts] scatter_points = 15000
tests/dtale/config/dtale_missing_props.ini+1 −0 modified@@ -6,3 +6,4 @@ hide_header_menu = False hide_main_menu = False hide_column_menus = False enable_custom_filters = False +enable_web_uploads = False
tests/dtale/config/test_config.py+6 −0 modified@@ -28,6 +28,7 @@ def test_load_app_settings(): "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)) @@ -45,6 +46,7 @@ def test_load_app_settings(): 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")) @@ -63,6 +65,7 @@ def test_load_app_settings(): 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 @@ -81,6 +84,7 @@ def test_load_app_settings_w_missing_props(): "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)) @@ -96,6 +100,7 @@ def test_load_app_settings_w_missing_props(): 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( @@ -112,6 +117,7 @@ def test_load_app_settings_w_missing_props(): 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
tests/dtale/test_upload.py+11 −0 modified@@ -128,6 +128,7 @@ 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( @@ -201,6 +202,16 @@ def test_web_upload(unittest): 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():
tests/dtale/test_views.py+3 −0 modified@@ -81,6 +81,7 @@ def test_startup(unittest): 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()) @@ -96,6 +97,7 @@ def test_startup(unittest): 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, @@ -121,6 +123,7 @@ def test_startup(unittest): 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,
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-7hfx-h3j3-rwq4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-21642ghsaADVISORY
- github.com/man-group/dtale/commit/954f6be1a06ff8629ead2c85c6e3f8e2196b3df2ghsax_refsource_MISCWEB
- github.com/man-group/dtale/security/advisories/GHSA-7hfx-h3j3-rwq4ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.