VYPR
Moderate severityNVD Advisory· Published Oct 25, 2023· Updated Sep 10, 2024

D-Tale vulnerable to Remote Code Execution through the Custom Filter Input

CVE-2023-46134

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.

PackageAffected versionsPatched versions
dtalePyPI
< 3.7.03.7.0

Affected products

2

Patches

1
bf8c54ab2490

Updated 'Custom Filter' feature to be disabled by default

https://github.com/man-group/dtaleAndrew Schonfeld (Boston)Oct 2, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.