Dangerous link detected Error in Dash Debug Window after upgrading from 2.14.2 to 2.15.0

I am trying to solve this ‘Dangerous Link created issue’ As per suggestion of @AnnMarieW , I am posting my code here.

import dash
import numpy as np
from dash import dcc, html
from dash.dash_table import DataTable
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output
from base64 import b64encode

# Demo DataFrame
data = {
    'Plastic': ['A', 'A', 'B', 'B', 'A', 'B', 'A', 'A', 'B', 'B', 'A', 'B'],
    'Make': [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
    'Block': [10, 20, 30, 10, 20, 30, 10, 20, 30, 10, 20, 30],
    'Measurement': [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
    'Actual_Yield': [85, 90, 88, 92, 87, 91, 85, 90, 88, 92, 87, 91],
    'Min': [80, 85, 85, 88, 82, 90, 80, 85, 85, 88, 82, 90],
    'Max': [88, 92, 90, 94, 89, 94, 88, 92, 90, 94, 89, 94]
}

df = pd.DataFrame(data)

# Calculate average yield for each grade and additional filters
average_yield_per_grade = df.groupby(['Plastic', 'Make', 'Block', 'Measurement'])['Actual_Yield'].mean().reset_index()

# Calculate overall average yield
overall_avg_yield = df['Actual_Yield'].mean()

app = dash.Dash(__name__)

app.layout = html.Div(className='row', children=[
    # Left column for Production Line Chart
    html.Div(className='six columns', children=[
        html.H1(children='KPI Dashboard', style={'textAlign': 'center'}),

        # Dropdown for selecting plastic grades
        dcc.Dropdown(
            id='plastic-dropdown',
            options=[
                {'label': plastic, 'value': plastic} for plastic in df['Plastic'].unique()
            ],
            value=df['Plastic'].iloc[0],  # Set default value to the first grade in the dataframe
            style={'width': '50%', 'margin': '20px auto'},
            placeholder='Select Plastic Grade'
        ),

        # Dropdown for selecting Make
        dcc.Dropdown(
            id='make-dropdown',
            options=[],  # Will be populated dynamically
            value='',  # Set default value to empty
            style={'width': '50%', 'margin': '20px auto'},
            placeholder='Select Make'
        ),

        # Dropdown for selecting Block
        dcc.Dropdown(
            id='block-dropdown',
            options=[],  # Will be populated dynamically
            value='',  # Set default value to empty
            style={'width': '50%', 'margin': '20px auto'},
            placeholder='Select Block'
        ),

        # Dropdown for selecting Measurement
        dcc.Dropdown(
            id='measurement-dropdown',
            options=[],  # Will be populated dynamically
            value='',  # Set default value to empty
            style={'width': '50%', 'margin': '20px auto'},
            placeholder='Select Measurement'
        ),

        # Line Chart
        dcc.Graph(
            id='production-line-chart',
            figure=px.line(title='Default Actual_Yield Trend')  # Set default figure
        ),
        # Download Button
        # html.Button('Download Data as Excel', id='download-link'),
        # dcc.Download(id="download-dataframe-csv"),

        dcc.Link('Download Data as Excel', id='download-link', href='/'),
        dcc.Download(id="download-dataframe-csv"),
    ]),

    # Center column for DataTable
    html.Div(className='four columns', children=[
        html.H3(children='Selected Combinations Data Table', style={'textAlign': 'center'}),

        DataTable(
            id='selected-data-table',
            columns=[
                {'name': 'Plastic', 'id': 'Plastic'},
                {'name': 'Make', 'id': 'Make'},
                {'name': 'Block', 'id': 'Block'},
                {'name': 'Measurement', 'id': 'Measurement'},
                {'name': 'Actual_Yield', 'id': 'Actual_Yield'},
                {'name': 'Min', 'id': 'Min'},
                {'name': 'Max', 'id': 'Max'},
                # Add more columns as needed
            ],
            style_table={'height': '300px', 'overflowY': 'auto'},
        ),
    ]),

    # Right column for Yield Gauge Charts
    html.Div(className='six columns', children=[
        # Yield Gauge Chart for Selected Grade
        html.Div(className='graph-container', children=[
            dcc.Graph(id='yield-gauge-chart-selected-grade',
                      style={'width': '700px', 'height': '500px', 'marginLeft': '280px'}
                      ),
        ]),

        # Yield Gauge Chart for Overall Average
        html.Div(className='graph-container', children=[
            dcc.Graph(id='yield-gauge-chart-overall-average',
                      style={'width': '700px', 'height': '500px', 'marginLeft': '-10px'}
                      ),
        ]),
    ], style={'display': 'flex'}),

    # Interval component for updating gauge value
    dcc.Interval(
        id='interval-component',
        interval=1 * 1000,  # in milliseconds (1 second)
        n_intervals=0
    ),
])


# Callback to populate Make dropdown based on selected plastic grade
@app.callback(
    Output('make-dropdown', 'options'),
    [Input('plastic-dropdown', 'value')]
)
def update_make_dropdown(selected_plastic):
    make_options = [{'label': str(make), 'value': make} for make in
                    df[df['Plastic'] == selected_plastic]['Make'].unique()]
    return make_options


# Callback to populate Block dropdown based on selected plastic grade and Make
@app.callback(
    Output('block-dropdown', 'options'),
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value')]
)
def update_block_dropdown(selected_plastic, selected_make):
    # Check if selected_make is not empty
    if not selected_make:
        return []

    block_options = [{'label': str(block), 'value': block} for block in
                     df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make))]['Block'].unique()]
    return block_options


# Callback to populate Measurement dropdown based on selected plastic grade, Make, and Block
@app.callback(
    Output('measurement-dropdown', 'options'),
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value'),
     Input('block-dropdown', 'value')]
)
def update_measurement_dropdown(selected_plastic, selected_make, selected_block):
    # Check if selected_block is not empty
    if not selected_block:
        return []

    measurement_options = [{'label': str(measurement), 'value': measurement} for measurement in
                           df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make)) &
                              (df['Block'] == float(selected_block))]['Measurement'].unique()]
    return measurement_options


# Callback to update line chart based on selected plastic grade and filters
@app.callback(
    Output('production-line-chart', 'figure'),
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value'),
     Input('block-dropdown', 'value'),
     Input('measurement-dropdown', 'value'),
     Input('interval-component', 'n_intervals')]
)
def update_line_chart(selected_plastic, selected_make, selected_block, selected_measurement, n_intervals):
    # Check if any selected value is empty
    if any(value is None or value == '' for value in
           [selected_plastic, selected_make, selected_block, selected_measurement]):
        return px.line(title='No data available')  # Return a default line chart with a title

    selected_df = df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make)) &
                     (df['Block'] == float(selected_block)) & (df['Measurement'] == float(selected_measurement))].copy()

    if selected_df.empty:
        return px.line(title='No data available')  # Return a default line chart with a title

    selected_df['Count'] = range(1, len(selected_df) + 1)

    # Check if 'Min' and 'Max' columns are present in the DataFrame
    if 'Min' not in selected_df.columns or 'Max' not in selected_df.columns:
        return px.line(title='Missing data columns')  # Return a default line chart with a title

    line_chart = px.line(selected_df, x='Count', y=['Min', 'Max'],
                         title=f'Plastic-{selected_plastic}, Make-{selected_make}, Block-{selected_block}, Measurement-{selected_measurement} Min'
                               f' and Max Trend')
    # Update marker properties for Min and Max
    line_chart.update_traces(
        line=dict(width=2),  # Adjust the width of the line
        mode='markers+lines',  # Show markers and lines
        marker=dict(size=10)  # Adjust the size of the marker
    )
    line_chart.update_layout(xaxis_title='Count')

    return line_chart


@app.callback(
    [Output('yield-gauge-chart-selected-grade', 'figure'),
     Output('yield-gauge-chart-overall-average', 'figure')],
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value'),
     Input('block-dropdown', 'value'),
     Input('measurement-dropdown', 'value'),
     Input('interval-component', 'n_intervals')]
)
def update_gauge_charts(selected_plastic, selected_make, selected_block, selected_measurement, n_intervals):
    # Check if any selected value is None or an empty string
    if any(value is None or value == '' for value in
           [selected_plastic, selected_make, selected_block, selected_measurement]):
        return go.Figure(), go.Figure()  # Return default figures

    # Check if selected_block and selected_measurement are not None or empty string before converting to float
    selected_block_float = float(selected_block) if selected_block and selected_block != '' else None
    selected_measurement_float = float(
        selected_measurement) if selected_measurement and selected_measurement != '' else None

    selected_df = df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make)) &
                     (df['Block'] == selected_block_float) & (df['Measurement'] == selected_measurement_float)].copy()

    if selected_df.empty:
        return go.Figure(), go.Figure()

    selected_df['Count'] = range(1, len(selected_df) + 1)

    current_yield = average_yield_per_grade.loc[
        (average_yield_per_grade['Plastic'] == selected_plastic) &
        (average_yield_per_grade['Make'] == int(selected_make)) &
        (average_yield_per_grade['Block'] == selected_block_float) &
        (average_yield_per_grade['Measurement'] == selected_measurement_float), 'Actual_Yield'].values

    if len(current_yield) > 0:
        current_yield = current_yield[0]
    else:
        current_yield = 0

    steps_selected_grade = [
        dict(range=[0, current_yield], color="green"),
        dict(range=[current_yield, 100], color="red")
    ]

    yield_gauge_chart_selected_grade = go.Figure(go.Indicator(
        mode='gauge+number',
        value=current_yield,
        title=f'Average Yield for Plastic {selected_plastic}',
        gauge=dict(
            axis=dict(range=[None, 100]),
            bar=dict(color="green"),
            steps=steps_selected_grade,
            threshold=dict(line=dict(color="red", width=2), thickness=0.75)
        )
    ))

    steps_overall_avg = [
        dict(range=[0, overall_avg_yield], color="green"),
        dict(range=[overall_avg_yield, 100], color="red")
    ]

    yield_gauge_chart_overall_avg = go.Figure(go.Indicator(
        mode='gauge+number',
        value=overall_avg_yield,
        title='Overall Average Yield',
        gauge=dict(
            axis=dict(range=[None, 100]),
            bar=dict(color="green"),
            steps=steps_overall_avg,
            threshold=dict(line=dict(color="red", width=2), thickness=0.75)
        )
    ))

    return yield_gauge_chart_selected_grade, yield_gauge_chart_overall_avg


@app.callback(
    [Output('selected-data-table', 'data'),
     Output('download-link', 'href')],
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value'),
     Input('block-dropdown', 'value'),
     Input('measurement-dropdown', 'value')]
)
def update_data_table(selected_plastic, selected_make, selected_block, selected_measurement):
    # Check if all selected values are not None or empty string
    if all(value is not None and value != '' for value in
           [selected_plastic, selected_make, selected_block, selected_measurement]):
        selected_df = df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make)) &
                         (df['Block'] == float(selected_block)) & (df['Measurement'] == float(selected_measurement))]

        # Convert DataFrame to CSV and encode as base64 for download link
        csv_string = selected_df.to_csv(index=False, encoding='utf-8')
        csv_base64 = 'data:text/csv;base64,' + b64encode(csv_string.encode()).decode()

        return selected_df.to_dict('records'), csv_base64
    else:
        return [], ''


# Run the app
if __name__ == '__main__':
    app.run_server(host='127.0.0.1', port=8058, debug=True)

Hi @Prasad1 and welcome to the Dash forum :slightly_smiling_face:

To download data, you can use the dcc.Download. Do not use the href of dcc.Link

Step 1
Change this:

dcc.Link('Download Data as Excel', id='download-link', href='/'),
dcc.Download(id="download-dataframe-csv"),

to:

html.Button("Download Data as  Excel", id="btn_xlsx"),
dcc.Download(id="download-dataframe-xlsx"),

Step 2

Add this callback. When the button is clicked it sends the data from the table to the dcc.Download component. This is very similar to the example in the docs.


@app.callback(
    Output("download-dataframe-xlsx", "data"),
    Input("btn_xlsx", "n_clicks"),
    State('selected-data-table', 'data'),
    prevent_initial_call=True,
)
def download(n_clicks, data):
    dff=pd.DataFrame(data)
    return dcc.send_data_frame(dff.to_excel, "mydf.xlsx", sheet_name="Sheet_name_1")


Step 3
Update this callback so it does not update the href with the dangerous link


@app.callback(
    Output('selected-data-table', 'data'),
    [Input('plastic-dropdown', 'value'),
     Input('make-dropdown', 'value'),
     Input('block-dropdown', 'value'),
     Input('measurement-dropdown', 'value')]
)
def update_data_table(selected_plastic, selected_make, selected_block, selected_measurement):
    # Check if all selected values are not None or empty string
    if all(value is not None and value != '' for value in
           [selected_plastic, selected_make, selected_block, selected_measurement]):
        selected_df = df[(df['Plastic'] == selected_plastic) & (df['Make'] == int(selected_make)) &
                         (df['Block'] == float(selected_block)) & (df['Measurement'] == float(selected_measurement))]

        return selected_df.to_dict('records')
    else:
        return []

3 Likes

I am seeing the same error in an app, where I embed a PDF as a base64 encoded string in an object element. Is there any way to work around this (introduced issue) for this case? :slight_smile:

import base64
import requests
from dash import Dash, html

# Get a sample PDF file.
r = requests.get("https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf")
bts = r.content
# Encode PDF file as base64 string.
encoded_string = base64.b64encode(bts).decode("ascii")
data= f"data:application/pdf;base64,{encoded_string}"
# Make a small example app.
app = Dash()
app.layout = html.ObjectEl(id="embedded-pdf", data=data, width="100%", height="100%", type='application/pdf')

if __name__ == '__main__':

app.run_server()

Hi Emil,

Starting in Dash 2.15, the urls are sanitized using Braintree’s sanitize-ur package. There isn’t much documentation but you can see what’s restricted based on the tests:

sanitize-url/src/__tests__/index.test.ts at main · braintree/sanitize-url · GitHub

Perhaps these are too restrictive? According to Mozilla, they allow certain data: urls

I can see that the chosen sanitizer doesn’t allow data: urls. At the top of my head, I would say that’s too restrictive - at least without an opt-out option like the dangerously_allow_html flag (and a deprecation notice, as this change breaks exsiting functionality).

As I understand, there is only an XSS vulnerability, if the user can inject information into the data element - which is not the case in my app. There is also some mention of the topic here,

… the @braintree/url-sanitize library. This library detects unsafe patterns and replaces them with about:blank . While this definitely works from a security perspective, a a closer look at the code again reveals a code pattern looking for known bad URLs. The pattern is also overly restrictive and considers all data: URLs to be off-limits, even though some are benign (e.g., images, audio, video, …).

I believe my use case falls in the condition noted in bold.

1 Like

I agree

I saw you opened an issue in GitHub - I referenced this forum discussion there too.

1 Like