D-Tale vulnerable to Remote Code Execution through the Custom Filter Input
Description
D-Tale versions before 3.7.0 allow remote code execution via the 'Custom Filter' input, enabling attackers to run malicious code on the server.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
D-Tale versions before 3.7.0 allow remote code execution via the 'Custom Filter' input, enabling attackers to run malicious code on the server.
D-Tale is a web-based tool for viewing and analyzing Pandas data structures, combining a Flask back-end and React front-end [1]. Prior to version 3.7.0, the 'Custom Filter' feature allowed users to input arbitrary pandas.query strings. This input was not properly sanitized, leading to a remote code execution (RCE) vulnerability [2][4].
An attacker can exploit this vulnerability by sending a crafted query string to a publicly accessible D-Tale instance. No authentication is required if the instance is exposed. The malicious query string is executed as Python code on the server, allowing the attacker to run arbitrary commands [4].
Successful exploitation enables an attacker to execute arbitrary code on the server, potentially leading to data exfiltration, server compromise, or further attacks on internal networks.
The vulnerability has been patched in D-Tale version 3.7.0, where the 'Custom Filter' input is disabled by default. Users are advised to upgrade to the latest version. For those unable to upgrade, the only workaround is to restrict access to trusted users only [2][4].
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.7.0 | 3.7.0 |
Affected products
2Patches
1bf8c54ab2490Updated 'Custom Filter' feature to be disabled by default
26 files changed · +206 −32
docs/CONFIGURATION.md+1 −0 modified@@ -25,6 +25,7 @@ lock_header_menu = False hide_header_menu = False hide_main_menu = False hide_column_menus = False +enable_custom_filters = False [charts] # this controls how many points can be contained within scatter & 3D charts scatter_points = 15000
dtale/app.py+3 −0 modified@@ -719,6 +719,8 @@ 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 :Example: @@ -804,6 +806,7 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options): 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"), ) 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@@ -107,6 +107,13 @@ def load_app_settings(config): section="app", getter="getboolean", ) + enable_custom_filters = get_config_val( + config, + curr_app_settings, + "enable_custom_filters", + section="app", + getter="getboolean", + ) open_custom_filter_on_startup = get_config_val( config, curr_app_settings, @@ -145,6 +152,7 @@ def load_app_settings(config): hide_header_menu=hide_header_menu, hide_main_menu=hide_main_menu, hide_column_menus=hide_column_menus, + enable_custom_filters=enable_custom_filters, ) ) @@ -214,6 +222,7 @@ def build_show_options(options=None): hide_header_menu=None, hide_main_menu=None, hide_column_menus=None, + enable_custom_filters=None, ) config_options = {} config = get_config()
dtale/global_state.py+15 −1 modified@@ -1,7 +1,7 @@ import string import inspect - +from logging import getLogger from six import PY3 from dtale.utils import dict_merge, format_data @@ -11,6 +11,8 @@ except ImportError: from collections import MutableMapping +logger = getLogger(__name__) + APP_SETTINGS = { "theme": "light", "pin_menu": False, @@ -30,6 +32,7 @@ "hide_header_menu": False, "hide_main_menu": False, "hide_column_menus": False, + "enable_custom_filters": False, } AUTH_SETTINGS = {"active": False, "username": None, "password": None} @@ -602,6 +605,17 @@ def set_app_settings(settings): instance_updates["hide_main_menu"] = settings.get("hide_main_menu") if settings.get("hide_column_menus") is not None: instance_updates["hide_column_menus"] = settings.get("hide_column_menus") + if settings.get("enable_custom_filters") is not None: + instance_updates["enable_custom_filters"] = settings.get( + "enable_custom_filters" + ) + if instance_updates["enable_custom_filters"]: + logger.warning( + ( + "Turning on custom filtering. Custom filters are vulnerable to code injection attacks, 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@@ -22,6 +22,7 @@ HIDE_HEADER_MENU = False HIDE_MAIN_MENU = False HIDE_COLUMN_MENUS = False + ENABLE_CUSTOM_FILTERS = False # flake8: NOQA from dtale.app import show, get_instance, instances, offline_chart # isort:skip
dtale/templates/dtale/base.html+1 −0 modified@@ -44,6 +44,7 @@ <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="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+27 −0 modified@@ -351,6 +351,7 @@ def update_settings(self, **updates): * 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 After applying please refresh any open browsers! """ @@ -906,6 +907,7 @@ def startup( hide_header_menu=None, hide_main_menu=None, hide_column_menus=None, + enable_custom_filters=None, force_save=True, ): """ @@ -1033,6 +1035,7 @@ def startup( hide_header_menu=hide_header_menu, hide_main_menu=hide_main_menu, hide_column_menus=hide_column_menus, + enable_custom_filters=enable_custom_filters, ) startup_code = ( "from arcticdb import Arctic\n" @@ -1104,6 +1107,7 @@ def startup( hide_header_menu=hide_header_menu, hide_main_menu=hide_main_menu, hide_column_menus=hide_column_menus, + enable_custom_filters=enable_custom_filters, ) global_state.set_dataset(instance._data_id, data) @@ -1171,6 +1175,8 @@ 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 column_edit_options is not None: base_settings["column_edit_options"] = column_edit_options global_state.set_settings(data_id, base_settings) @@ -1218,6 +1224,13 @@ def startup( global_state.set_context_variables( data_id, build_context_variables(data_id, context_vars) ) + if global_state.load_flag(data_id, "enable_custom_filters", False): + logger.warning( + ( + "Custom filtering enabled. Custom filters are vulnerable to code injection attacks, 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!") @@ -1251,6 +1264,9 @@ def base_render_template(template, data_id, **kwargs): hide_header_menu = global_state.load_flag(data_id, "hide_header_menu", False) 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 + ) app_overrides = dict( allow_cell_edits=json.dumps(allow_cell_edits), hide_shutdown=hide_shutdown, @@ -1259,6 +1275,7 @@ def base_render_template(template, data_id, **kwargs): hide_header_menu=hide_header_menu, hide_main_menu=hide_main_menu, hide_column_menus=hide_column_menus, + enable_custom_filters=enable_custom_filters, github_fork=github_fork, ) is_arcticdb = 0 @@ -1996,6 +2013,16 @@ def test_filter(data_id): :return: JSON {success: True/False} """ query = get_str_arg(request, "query") + if query and not global_state.load_flag(data_id, "enable_custom_filters", False): + return jsonify( + dict( + success=False, + error=( + "Custom Filters not enabled! Custom filters are vulnerable to code injection attacks, please only " + "use in trusted environments." + ), + ) + ) run_query( handle_predefined(data_id), build_query(data_id, query),
frontend/static/dtale/export/main.tsx+1 −0 modified@@ -23,6 +23,7 @@ actions.loadLockHeaderMenu(store); actions.loadHideHeaderMenu(store); actions.loadHideMainMenu(store); actions.loadHideColumnMenus(store); +actions.loadEnableCustomFilters(store); const root = ReactDOMClient.createRoot(document.getElementById('content')!); root.render( <Provider store={store}>
frontend/static/main.tsx+1 −0 modified@@ -48,6 +48,7 @@ let storeBuilder: () => Store = () => { actions.loadHideHeaderMenu(store); actions.loadHideMainMenu(store); actions.loadHideColumnMenus(store); + actions.loadEnableCustomFilters(store); return store; }; if (pathname.indexOf('/dtale/popup') === 0) {
frontend/static/popups/filter/FilterPanel.tsx+14 −15 modified@@ -1,4 +1,3 @@ -import { createSelector } from '@reduxjs/toolkit'; import * as React from 'react'; import { WithTranslation, withTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -11,24 +10,19 @@ import SidePanelButtons from '../../dtale/side/SidePanelButtons'; import { ActionType, HideSidePanelAction, SetQueryEngineAction } from '../../redux/actions/AppActions'; import * as dtaleActions from '../../redux/actions/dtale'; import * as settingsActions from '../../redux/actions/settings'; -import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors'; import { InstanceSettings, QueryEngine } from '../../redux/state/AppState'; import { RemovableError } from '../../RemovableError'; import * as CustomFilterRepository from '../../repository/CustomFilterRepository'; import { Checkbox } from '../create/LabeledCheckbox'; import ContextVariables from './ContextVariables'; +import { DISABLED_CUSTOM_FILTERS_MSG, selectResult } from './FilterPopup'; import PandasQueryHelp from './PandasQueryHelp'; import QueryExamples from './QueryExamples'; import StructuredFilters from './StructuredFilters'; -const selectResult = createSelector( - [selectDataId, selectQueryEngine, selectSettings], - (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }), -); - const FilterPanel: React.FC<WithTranslation> = ({ t }) => { - const { dataId, queryEngine, settings } = useSelector(selectResult); + const { dataId, enableCustomFilters, queryEngine, settings } = useSelector(selectResult); const dispatch = useDispatch(); const hideSidePanel = (): HideSidePanelAction => dispatch({ type: ActionType.HIDE_SIDE_PANEL }); const updateSettings = (updatedSettings: Partial<InstanceSettings>, callback?: () => void): AnyAction => @@ -109,17 +103,22 @@ const FilterPanel: React.FC<WithTranslation> = ({ t }) => { <div className="row m-0 pb-3"> <div className="col p-0 font-weight-bold mt-auto">{t('Custom Filter', { ns: 'filter' })}</div> <PandasQueryHelp /> - <button className="btn btn-primary col-auto pt-2 pb-2" onClick={clear}> - <span>{t('Clear', { ns: 'filter' })}</span> - </button> - <button className="btn btn-primary col-auto pt-2 pb-2" onClick={save}> - <span>{t('Apply', { ns: 'filter' })}</span> - </button> + {enableCustomFilters && ( + <> + <button className="btn btn-primary col-auto pt-2 pb-2" onClick={clear}> + <span>{t('Clear', { ns: 'filter' })}</span> + </button> + <button className="btn btn-primary col-auto pt-2 pb-2" onClick={save}> + <span>{t('Apply', { ns: 'filter' })}</span> + </button> + </> + )} </div> <textarea style={{ width: '100%', height: 150 }} - value={query || ''} + value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG} onChange={(event) => setQuery(event.target.value)} + disabled={!enableCustomFilters} /> </div> </div>
frontend/static/popups/filter/FilterPopup.tsx+26 −12 modified@@ -11,7 +11,7 @@ import { CloseChartAction, SetQueryEngineAction } from '../../redux/actions/AppA import { closeChart } from '../../redux/actions/charts'; import * as dtaleActions from '../../redux/actions/dtale'; import * as settingsActions from '../../redux/actions/settings'; -import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors'; +import { selectDataId, selectEnableCustomFilters, selectQueryEngine, selectSettings } from '../../redux/selectors'; import { InstanceSettings, QueryEngine } from '../../redux/state/AppState'; import { RemovableError } from '../../RemovableError'; import * as CustomFilterRepository from '../../repository/CustomFilterRepository'; @@ -22,13 +22,22 @@ import PandasQueryHelp from './PandasQueryHelp'; import QueryExamples from './QueryExamples'; import StructuredFilters from './StructuredFilters'; -const selectResult = createSelector( - [selectDataId, selectQueryEngine, selectSettings], - (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }), +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', +].join(''); + +export const selectResult = createSelector( + [selectDataId, selectQueryEngine, selectEnableCustomFilters, selectSettings], + (dataId, queryEngine, enableCustomFilters, settings) => ({ dataId, queryEngine, enableCustomFilters, settings }), ); const FilterPopup: React.FC<WithTranslation> = ({ t }) => { - const { dataId, queryEngine, settings } = useSelector(selectResult); + const { dataId, queryEngine, enableCustomFilters, settings } = useSelector(selectResult); const dispatch = useDispatch(); const onClose = (): CloseChartAction => dispatch(closeChart()); const updateSettings = (updatedSettings: Partial<InstanceSettings>, callback?: () => void): AnyAction => @@ -140,8 +149,9 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => { <div className="font-weight-bold pt-3 pb-3">{t('Custom Filter', { ns: 'filter' })}</div> <textarea style={{ width: '100%', height: 150 }} - value={query || ''} + value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG} onChange={(event) => setQuery(event.target.value)} + disabled={!enableCustomFilters} /> </div> </div> @@ -165,12 +175,16 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => { </div> <div className="modal-footer"> <PandasQueryHelp /> - <button className="btn btn-primary" onClick={clear}> - <span>{t('Clear', { ns: 'filter' })}</span> - </button> - <button className="btn btn-primary" onClick={save}> - <span>{t('Apply', { ns: 'filter' })}</span> - </button> + {enableCustomFilters && ( + <> + <button className="btn btn-primary" onClick={clear}> + <span>{t('Clear', { ns: 'filter' })}</span> + </button> + <button className="btn btn-primary" onClick={save}> + <span>{t('Apply', { ns: 'filter' })}</span> + </button> + </> + )} </div> </React.Fragment> );
frontend/static/redux/actions/AppActions.ts+8 −1 modified@@ -63,6 +63,7 @@ export enum ActionType { UPDATE_HIDE_HEADER_MENU = 'update-hide-header-menu', UPDATE_HIDE_MAIN_MENU = 'update-hide-main-menu', UPDATE_HIDE_COLUMN_MENUS = 'update-hide-column-menus', + UPDATE_ENABLE_CUSTOM_FILTERS = 'update-enable-custom-filters', } /** Action fired when a range is selected */ @@ -257,6 +258,11 @@ export interface UpdateHideColumnMenus extends Action<typeof ActionType.UPDATE_H value: boolean; } +/** Action fired when updating the enable_custom_filters flag */ +export interface UpdateEnableCustomFilters extends Action<typeof ActionType.UPDATE_ENABLE_CUSTOM_FILTERS> { + value: boolean; +} + /** Type definition encompassing all application actions */ export type AppActionTypes = | InitAction @@ -302,7 +308,8 @@ export type AppActionTypes = | UpdateLockHeaderMenu | UpdateHideHeaderMenu | UpdateHideMainMenu - | UpdateHideColumnMenus; + | UpdateHideColumnMenus + | UpdateEnableCustomFilters; /** Type definition for redux application actions */ export type AppActions<R> = ThunkAction<R, AppState, Record<string, unknown>, AnyAction>;
frontend/static/redux/actions/dtale.ts+8 −0 modified@@ -77,6 +77,14 @@ export const loadHideColumnMenus = (store: Store<AppState, AnyAction>): void => }); }; +export const loadEnableCustomFilters = (store: Store<AppState, AnyAction>): void => { + const { settings, enableCustomFilters } = store.getState(); + store.dispatch({ + type: ActionType.UPDATE_ENABLE_CUSTOM_FILTERS, + value: enableCustomFilters ?? settings.enable_custom_filters ?? enableCustomFilters, + }); +}; + export const openCustomFilter = (): SidePanelAction => ({ type: ActionType.SHOW_SIDE_PANEL, view: SidePanelType.FILTER,
frontend/static/redux/reducers/app/settings.ts+13 −0 modified@@ -80,6 +80,19 @@ export const hideColumnMenus = (state = false, action: AppActionTypes): boolean } }; +export const enableCustomFilters = (state = false, action: AppActionTypes): boolean => { + switch (action.type) { + case ActionType.INIT_PARAMS: + return toBool(getHiddenValue('enable_custom_filters')); + case ActionType.UPDATE_ENABLE_CUSTOM_FILTERS: + return action.value; + 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+9 −0 modified@@ -58,6 +58,7 @@ export const selectBaseLockHeaderMenu = (state: AppState): boolean => state.lock 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 selectFilteredRanges = (state: AppState): FilteredRanges => state.filteredRanges; export const selectShowAllHeatmapColumns = (state: AppState): boolean => state.showAllHeatmapColumns; export const selectChartData = (state: AppState): Popups => state.chartData; @@ -106,6 +107,14 @@ export const selectHideColumnMenus = createSelector( [selectSettingsHideColumnMenus, selectBaseHideColumnMenus], (settingsHideColumnMenus, hideColumnMenus) => settingsHideColumnMenus ?? hideColumnMenus, ); +const selectSettingsEnableCustomFilters = createSelector( + [selectSettings], + (settings) => settings?.enable_custom_filters, +); +export const selectEnableCustomFilters = createSelector( + [selectSettingsEnableCustomFilters, selectBaseEnableCustomFilters], + (settingsEnableCustomFilters, enableCustomFilters) => settingsEnableCustomFilters ?? enableCustomFilters, +); export const selectRibbonMenuOpen = createSelector( [selectBaseRibbonMenuOpen, selectLockHeaderMenu, selectHideHeaderMenu], (ribbonMenuOpen, lockHeaderMenu, hideHeaderMenu) => (ribbonMenuOpen || lockHeaderMenu) && !hideHeaderMenu,
frontend/static/redux/state/AppState.ts+3 −0 modified@@ -382,6 +382,7 @@ export interface InstanceSettings { hide_main_menu: boolean; hide_column_menus: boolean; isArcticDB?: number; + enable_custom_filters: boolean; } export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({ @@ -395,6 +396,7 @@ export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({ hide_header_menu: false, hide_main_menu: false, hide_column_menus: false, + enable_custom_filters: false, }); /** Type definition for semantic versioning of python */ @@ -437,6 +439,7 @@ export interface AppSettings { hideHeaderMenu: boolean; hideMainMenu: boolean; hideColumnMenus: boolean; + enableCustomFilters: boolean; } /** Properties for specifying filtered ranges */
frontend/static/__tests__/dtale/DataViewer-filter-test.tsx+13 −2 modified@@ -5,6 +5,7 @@ import { Provider } from 'react-redux'; import { Store } from 'redux'; import { DataViewer } from '../../dtale/DataViewer'; +import { DISABLED_CUSTOM_FILTERS_MSG } from '../../popups/filter/FilterPopup'; import DimensionsHelper from '../DimensionsHelper'; import reduxUtils from '../redux-test-utils'; import { buildInnerHTML, clickMainMenuButton, mockChartJS } from '../test-utils'; @@ -46,9 +47,9 @@ describe('FilterPanel', () => { await clickMainMenuButton('Custom Filter'); }; - const buildResult = async (dataId = '1'): Promise<void> => { + const buildResult = async (dataId = '1', overrides?: Record<string, string>): Promise<void> => { store = reduxUtils.createDtaleStore(); - buildInnerHTML({ settings: '', dataId }, store); + buildInnerHTML({ settings: '', dataId, enableCustomFilters: 'True', ...overrides }, store); await act(() => { const result = render( <Provider store={store}> @@ -135,4 +136,14 @@ describe('FilterPanel', () => { }); expect(screen.queryByTestId('structured-filters')).toBeNull(); }); + + it('DataViewer: filtering with custom filtering not enabled', async () => { + await buildResult('1', { enableCustomFilters: 'False' }); + const textarea = screen.getByTestId('filter-panel').getElementsByTagName('textarea')[0]; + expect(textarea.value).toBe(DISABLED_CUSTOM_FILTERS_MSG); + expect(textarea).toBeDisabled(); + const buttons = [...screen.getByTestId('filter-panel').querySelectorAll('button')]; + expect(buttons.filter((b) => b.textContent === 'Apply')).toHaveLength(0); + expect(buttons.filter((b) => b.textContent === 'Clear')).toHaveLength(0); + }); });
frontend/static/__tests__/dtale/reduxGridUtils-test.ts+1 −0 modified@@ -23,6 +23,7 @@ describe('reduxGridUtils', () => { hide_header_menu: false, hide_main_menu: false, hide_column_menus: false, + enable_custom_filters: false, }; reduxUtils.handleReduxState( columns,
frontend/static/__tests__/popups/filter/FilterPopup-test.tsx+1 −1 modified@@ -40,7 +40,7 @@ describe('FilterPopup', () => { updateQueryEngineSpy = jest.spyOn(serverState, 'updateQueryEngine'); updateQueryEngineSpy.mockResolvedValue(Promise.resolve({ success: true })); store = reduxUtils.createDtaleStore(); - buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python' }, store); + buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python', enableCustomFilters: 'True' }, store); store.dispatch({ type: ActionType.OPEN_CHART, chartData: { visible: true } }); wrapper = await act( async () =>
frontend/static/__tests__/reducers/dtale-test.tsx+2 −0 modified@@ -28,6 +28,7 @@ describe('reducer tests', () => { hideHeaderMenu: false, hideMainMenu: false, hideColumnMenus: false, + enableCustomFilters: false, hideDropRows: false, iframe: false, columnMenuOpen: false, @@ -52,6 +53,7 @@ describe('reducer tests', () => { hide_header_menu: false, hide_main_menu: false, hide_column_menus: false, + enable_custom_filters: false, }, pythonVersion: null, isPreview: false,
frontend/static/__tests__/test-utils.tsx+1 −0 modified@@ -95,6 +95,7 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s 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), BASE_HTML, ].join(''); store?.dispatch(actions.init());
README.md+14 −0 modified@@ -976,6 +976,20 @@ outliers = s[(s < iqr_lower) | (s > iqr_upper)] If you click on the "Apply outlier filter" link this will add an addtional "outlier" filter for this column which can be removed from the [header](#header) or the [custom filter](#custom-filter) shown in picture above to the right. #### Custom Filter + +**Starting with version 3.7.0 this feature will be turned off by default. +Custom filters are vulnerable to code injection attacks, please only use in trusted environments.** + +**You can turn this feature on by doing one of the following:** + - **add `enable_custom_filters=True` to your `dtale.show` call** + - **add `enable_custom_filters = 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_custom_filters=True)) +``` + + Apply a custom pandas `query` to your data (link to pandas documentation included in popup) |Editing|Result|
tests/dtale/config/dtale.ini+1 −0 modified@@ -13,6 +13,7 @@ lock_header_menu = False hide_header_menu = False hide_main_menu = False hide_column_menus = False +enable_custom_filters = False [charts] scatter_points = 15000
tests/dtale/config/dtale_missing_props.ini+1 −0 modified@@ -5,3 +5,4 @@ lock_header_menu = False hide_header_menu = False hide_main_menu = False hide_column_menus = False +enable_custom_filters = False
tests/dtale/config/test_config.py+6 −0 modified@@ -27,6 +27,7 @@ def test_load_app_settings(): "hide_header_menu": True, "hide_main_menu": True, "hide_column_menus": True, + "enable_custom_filters": True, } with ExitStack() as stack: stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings)) @@ -43,6 +44,7 @@ def test_load_app_settings(): assert settings["hide_header_menu"] assert settings["hide_main_menu"] assert settings["hide_column_menus"] + assert settings["enable_custom_filters"] load_app_settings( load_config_state(os.path.join(os.path.dirname(__file__), "dtale.ini")) @@ -60,6 +62,7 @@ def test_load_app_settings(): assert not settings["hide_header_menu"] assert not settings["hide_main_menu"] assert not settings["hide_column_menus"] + assert not settings["enable_custom_filters"] @pytest.mark.unit @@ -77,6 +80,7 @@ def test_load_app_settings_w_missing_props(): "hide_header_menu": True, "hide_main_menu": True, "hide_column_menus": True, + "enable_custom_filters": True, } with ExitStack() as stack: stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings)) @@ -91,6 +95,7 @@ def test_load_app_settings_w_missing_props(): assert settings["hide_header_menu"] assert settings["hide_main_menu"] assert settings["hide_column_menus"] + assert settings["enable_custom_filters"] load_app_settings( load_config_state( @@ -106,6 +111,7 @@ def test_load_app_settings_w_missing_props(): assert not settings["hide_header_menu"] assert not settings["hide_main_menu"] assert not settings["hide_column_menus"] + assert not settings["enable_custom_filters"] @pytest.mark.unit
tests/dtale/test_views.py+26 −0 modified@@ -25,6 +25,14 @@ app = build_app(url=URL) +def setup_function(function): + global_state.cleanup() + + +def teardown_function(function): + global_state.cleanup() + + @pytest.mark.unit def test_head_endpoint(): import dtale.views as views @@ -72,6 +80,7 @@ def test_startup(unittest): hide_header_menu=True, hide_main_menu=True, hide_column_menus=True, + enable_custom_filters=True, ) pdt.assert_frame_equal(instance.data, test_data.reset_index()) @@ -86,6 +95,7 @@ def test_startup(unittest): hide_header_menu=True, hide_main_menu=True, hide_column_menus=True, + enable_custom_filters=True, locked=["date", "security_id"], indexes=["date", "security_id"], precision=2, @@ -110,6 +120,7 @@ def test_startup(unittest): hide_header_menu=True, hide_main_menu=True, hide_column_menus=True, + enable_custom_filters=True, locked=["date", "security_id"], indexes=["date", "security_id"], precision=2, @@ -1329,6 +1340,7 @@ def _df(): def test_test_filter(test_data): with app.test_client() as c: build_data_inst({c.port: test_data}) + global_state.set_app_settings(dict(enable_custom_filters=True)) response = c.get( "/dtale/test-filter/{}".format(c.port), query_string=dict(query="date == date"), @@ -1375,6 +1387,20 @@ def test_test_filter(test_data): ) response_data = response.get_json() assert response_data["success"] + + global_state.set_app_settings(dict(enable_custom_filters=False)) + response = c.get( + "/dtale/test-filter/{}".format(c.port), + query_string=dict(query="foo2 == 1", save=True), + ) + response_data = response.get_json() + assert not response_data["success"] + assert response_data["error"] == ( + "Custom Filters not enabled! Custom filters are vulnerable to code injection attacks, please only " + "use in trusted environments." + ) + global_state.set_app_settings(dict(enable_custom_filters=True)) + if PY3: df = pd.DataFrame([dict(a=1)]) df["a.b"] = 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-jq6c-r9xf-qxjmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-46134ghsaADVISORY
- github.com/man-group/dtale/commit/bf8c54ab2490803f45f0652a9a0e221a94d39668ghsax_refsource_MISCWEB
- github.com/man-group/dtale/security/advisories/GHSA-jq6c-r9xf-qxjmghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.