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

Hi Emil,

i am having the same issue with PDFs being read from a database as base64 encoded string. I tried to display them with html.Iframe but i get the Dangerous link detected error. Have you managed to solve this or figure out a workaround?

Thanks in advance!

Hey all,

Have you considered having a flask template that is allowed to pull the data: from a file and place it into the template?

At which point you just pull the specific flask template into an iframe? Thus avoiding the vulnerability?

As an example:

from dash import *

app = Dash(__name__)

server = app.server

@server.route('/testfile', methods=['GET'])
def testfile():
    return """<html><body style="width:100%; height:100%;"><iframe style="width:100%; height:100%; border:0;" scrolling="no"
    src="data:image/png;base64,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"/>
    </body></html>"""


app.layout = html.Iframe(src='./testfile', style={'height': '100%', 'width': '100%', 'overflow': 'auto'})

app.run(debug=True)

image

1 Like

That’s a cool hack :smile:

1 Like

I wouldnt say its a hack, haha. Just using whats available, lol.

Hey jinnyzor!

Can you tell me how I can implement this logic on a subpage in a multipage app?

Thanks!

Same thing, basically the flask endpoint is where you have all the data: pulling from, you just pass it some identifier to query the data from wherever you want to pull (db or filesystem) and return the data: to it like normal.

Then, on the page, just pass the iframe like I did above, keep in mind that you need the absolute browser route to the file. ../ results in two steps backwards. Or you could use the request to get the host_url or host_name and append the route to there.

If anyone would like to use this approach I went with in a multipage app:

layout = html.Div(
    children=[
        html.Button(
            id="dummy-dash-btn",
        ),
        html.Iframe(
            id="dummy-dash-output",
            style={"height": "100%", "width": "100%", "overflow": "auto"},
        ),
    ],
)


# Read - PDF
@callback(
    Output("dummy-dash-output", "srcDoc"),
    Input("dummy-dash-btn", "n_clicks"),
    prevent_initial_call=True,
)
def dash_read_base64(n_clicks):
    if n_clicks:
        content = "data:image/png;base64,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"

        srcDoc = """<html><body style="width:100%; height:100%;"><iframe style="width:100%; height:100%; border:0;" scrolling="no" src="{content}"/></body></html>""".format(
            content=content
        )

        return srcDoc

    raise PreventUpdate

You can simply retrive base64 encoded string from wherever and save it as content variable.

1 Like

Hello @dashamateur,

While this works as a workaround for now.

I believe this exposes another security concern that the sanitizer was originally designed to work against.

I think if you go this route, it may not exist is future releases.

The Braintree sanitize-ur package in 2.15.0 broke functionality in my app where a user uploads a .pdf and is able to view the PDF before making a decision to upload it to our cloud storage. I am also receiving the dangerous link error.

Hi @Bw984

This has been fixed and will be available in the next release (2.17). It should be available in a few days. :slight_smile:

1 Like

That is great to hear! I will be sure to update our dependencies as soon as the release has been made as long as there are no other breaking changes.

Thanks!