dash-uploader-ng 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_uploader_ng-1.0.0/LICENSE.txt +21 -0
- dash_uploader_ng-1.0.0/MANIFEST.in +11 -0
- dash_uploader_ng-1.0.0/PKG-INFO +84 -0
- dash_uploader_ng-1.0.0/README.md +199 -0
- dash_uploader_ng-1.0.0/SECURITY.md +140 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/__init__.py +59 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/Button.py +67 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/ProgressBar.py +58 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/Upload_ReactComponent.py +181 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/__init__.py +0 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/_imports_.py +9 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/dash_uploader_ng.min.js +3 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/dash_uploader_ng.min.js.LICENSE.txt +3 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/dash_uploader_ng.min.js.map +1 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/metadata.json +1 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/_build/package-info.json +59 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/callbacks.py +135 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/configure_upload.py +110 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/httprequesthandler.py +334 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/safepath.py +194 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/settings.py +20 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/upload.py +178 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/uploadstatus.py +82 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng/utils.py +67 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng.egg-info/PKG-INFO +84 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng.egg-info/SOURCES.txt +36 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng.egg-info/dependency_links.txt +1 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng.egg-info/requires.txt +6 -0
- dash_uploader_ng-1.0.0/dash_uploader_ng.egg-info/top_level.txt +1 -0
- dash_uploader_ng-1.0.0/docs/CHANGELOG.md +118 -0
- dash_uploader_ng-1.0.0/docs/README-PyPi.md +52 -0
- dash_uploader_ng-1.0.0/package.json +59 -0
- dash_uploader_ng-1.0.0/pyproject.toml +96 -0
- dash_uploader_ng-1.0.0/setup.cfg +4 -0
- dash_uploader_ng-1.0.0/tests/test_safepath.py +174 -0
- dash_uploader_ng-1.0.0/tests/test_security_cve_2026_38360.py +305 -0
- dash_uploader_ng-1.0.0/tests/test_uploadstatus.py +22 -0
- dash_uploader_ng-1.0.0/tests/test_upstream_parity.py +363 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020- Niko Föhr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
include dash_uploader_ng/_build/dash_uploader_ng.min.js
|
|
2
|
+
include dash_uploader_ng/_build/dash_uploader_ng.min.js.map
|
|
3
|
+
include dash_uploader_ng/_build/dash_uploader_ng.min.js.LICENSE.txt
|
|
4
|
+
include dash_uploader_ng/_build/metadata.json
|
|
5
|
+
include dash_uploader_ng/_build/package-info.json
|
|
6
|
+
include README.md
|
|
7
|
+
include SECURITY.md
|
|
8
|
+
include docs/README-PyPi.md
|
|
9
|
+
include docs/CHANGELOG.md
|
|
10
|
+
include LICENSE.txt
|
|
11
|
+
include package.json
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-uploader-ng
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Large file upload component for Dash -- a maintained, security-patched fork of dash-uploader
|
|
5
|
+
Author-email: Niko Föhr <fohrloop@gmail.com>
|
|
6
|
+
Maintainer: Awwab Mahdi, Claude (Anthropic)
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/obliviance/dash-uploader-ng
|
|
9
|
+
Project-URL: Repository, https://github.com/obliviance/dash-uploader-ng
|
|
10
|
+
Project-URL: Issues, https://github.com/obliviance/dash-uploader-ng/issues
|
|
11
|
+
Project-URL: Security, https://github.com/obliviance/dash-uploader-ng/blob/main/SECURITY.md
|
|
12
|
+
Keywords: dash,upload,plotly,flow.js,file-upload
|
|
13
|
+
Classifier: Framework :: Dash
|
|
14
|
+
Classifier: Framework :: Flask
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
Requires-Dist: dash>=2.0
|
|
27
|
+
Requires-Dist: packaging>=21.0
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
30
|
+
Requires-Dist: flask>=2.0; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
   
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
# 📤 dash-uploader-ng
|
|
39
|
+
The upload package for [Dash](https://dash.plotly.com/) applications using large data files. A maintained, security-patched fork of [dash-uploader](https://github.com/fohrloop/dash-uploader).
|
|
40
|
+
|
|
41
|
+
### 🏠 Homepage & Documentation
|
|
42
|
+
[https://github.com/obliviance/dash-uploader-ng](https://github.com/obliviance/dash-uploader-ng)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Short summary
|
|
46
|
+
💾 Data file size has no limits. (Except the hard disk size)<bR>
|
|
47
|
+
☎ Call easily a callback after uploading is finished.<br>
|
|
48
|
+
📦 Upload files using [flow.js](https://github.com/flowjs/flow.js/)
|
|
49
|
+
<br>
|
|
50
|
+
✅ Works with Dash 2.0+ (verified on Dash 4.x) & Python 3.10+.<br>
|
|
51
|
+
🔒 Security-patched fork of dash-uploader (fixes CVE-2026-38360).<br>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Installing
|
|
56
|
+
```
|
|
57
|
+
pip install dash-uploader-ng
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Simple example
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import dash
|
|
67
|
+
from dash import html
|
|
68
|
+
import dash_uploader_ng as du
|
|
69
|
+
|
|
70
|
+
app = dash.Dash(__name__)
|
|
71
|
+
|
|
72
|
+
# 1) configure the upload folder
|
|
73
|
+
du.configure_upload(app, r"C:\tmp\Uploads")
|
|
74
|
+
|
|
75
|
+
# 2) Use the Upload component
|
|
76
|
+
app.layout = html.Div([
|
|
77
|
+
du.Upload(),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
if __name__ == '__main__':
|
|
81
|
+
app.run(debug=True)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# 📤 dash-uploader-ng
|
|
2
|
+
|
|
3
|
+
A maintained, **security-patched** fork of
|
|
4
|
+
[dash-uploader](https://github.com/fohrloop/dash-uploader) — the upload
|
|
5
|
+
component for [Dash](https://dash.plotly.com/) applications that need large
|
|
6
|
+
data files.
|
|
7
|
+
|
|
8
|
+
Upstream dash-uploader was archived on 2025-07-19 without a fix for
|
|
9
|
+
[CVE-2026-38360](SECURITY.md), a critical (CVSS 9.8) path-traversal → RCE in
|
|
10
|
+
the upload endpoint. This fork **fixes that vulnerability**, modernizes the
|
|
11
|
+
packaging, and refreshes the dependency tree while keeping the same API.
|
|
12
|
+
|
|
13
|
+
> **Migrating from `dash-uploader`:** the import name changed, so update your
|
|
14
|
+
> imports. Nothing else in your app needs to change.
|
|
15
|
+
> ```diff
|
|
16
|
+
> - import dash_uploader as du
|
|
17
|
+
> + import dash_uploader_ng as du
|
|
18
|
+
> ```
|
|
19
|
+
|
|
20
|
+
- 🔒 **CVE-2026-38360 fixed** — see [SECURITY.md](SECURITY.md).
|
|
21
|
+
- 🧰 **Modernized** — `pyproject.toml`, Python 3.10–3.13, `dash>=2.0` (verified
|
|
22
|
+
on dash 4.x), zero known npm vulnerabilities in the build.
|
|
23
|
+
- 🤝 **Drop-in API** — same `configure_upload` / `Upload` / `callback` /
|
|
24
|
+
`UploadStatus` as upstream.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Table of contents
|
|
29
|
+
- [Short summary](#short-summary)
|
|
30
|
+
- [dash-uploader vs. dcc.Upload](#dash-uploader-vs-dccupload)
|
|
31
|
+
- [Installing](#installing)
|
|
32
|
+
- [Quickstart](#quickstart)
|
|
33
|
+
- [Simple example](#simple-example)
|
|
34
|
+
- [Example with callback](#example-with-callback-and-other-options)
|
|
35
|
+
- [Contributing](#contributing)
|
|
36
|
+
- [Documentation](#documentation)
|
|
37
|
+
- [Changelog](#changelog)
|
|
38
|
+
- [Credits](#credits)
|
|
39
|
+
|
|
40
|
+
## Short summary
|
|
41
|
+
💾 Data file size has no limits. (Except the hard disk size)<bR>
|
|
42
|
+
☎ Call easily a callback after uploading is finished.<br>
|
|
43
|
+
📤 Upload files using [flow.js](https://github.com/flowjs/flow.js/) <br>
|
|
44
|
+
📦 All JS and CSS bundled with the package. No need for network calls for CSS/JS.<br>
|
|
45
|
+
✅ Works with Dash 2.0+ (verified on Dash 4.x) & Python 3.10+.<br>
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### dash-uploader vs. [dcc.Upload](https://dash.plotly.com/dash-core-components/upload)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
| | dash-uploader | [dcc.Upload](https://dash.plotly.com/dash-core-components/upload) |
|
|
55
|
+
| --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
56
|
+
| Underlying technology | [flow.js](https://github.com/flowjs/flow.js/) | HTML5 API |
|
|
57
|
+
| File size | Unlimited | max ~150-200MB ([link](https://community.plotly.com/t/dash-upload-component-decoding-large-files/8033/11)) |
|
|
58
|
+
| Uploads to | Hard disk (server side) | First to browser memory (user side) Then, to server using callbacks. |
|
|
59
|
+
| Data type | Uploaded as file; no need to parse at server side. | Uploaded as byte64 encoded string -> Needs parsing |
|
|
60
|
+
| See upload progress? | Progressbar out of the box | No upload indicators out of the box. Generic loading indicator possible. [Progressbar not possible](https://community.plotly.com/t/upload-after-confirmation-and-progress-bar/7172). |
|
|
61
|
+
|
|
62
|
+
# Installing
|
|
63
|
+
```
|
|
64
|
+
pip install dash-uploader-ng
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
# Quickstart
|
|
68
|
+
|
|
69
|
+
Full documentation [here](docs/dash-uploader.md)
|
|
70
|
+
>⚠️**Security note**: The Upload component allows POST requests and uploads of arbitrary files to the server harddisk and one should take this into account (with user token checking etc.) if used as part of a public website! For this you can utilize the `http_request_handler` argument of the [du.configure_upload](https://github.com/fohrloop/dash-uploader/blob/master/docs/dash-uploader.md#duconfigure_upload). (New in version 0.5.0)
|
|
71
|
+
|
|
72
|
+
## Simple example
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import dash
|
|
76
|
+
from dash import html
|
|
77
|
+
import dash_uploader_ng as du
|
|
78
|
+
|
|
79
|
+
app = dash.Dash(__name__)
|
|
80
|
+
|
|
81
|
+
# 1) configure the upload folder
|
|
82
|
+
du.configure_upload(app, r"C:\tmp\Uploads")
|
|
83
|
+
|
|
84
|
+
# 2) Use the Upload component
|
|
85
|
+
app.layout = html.Div([
|
|
86
|
+
du.Upload(),
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
if __name__ == '__main__':
|
|
90
|
+
app.run(debug=True)
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Example with callback (and other options)
|
|
95
|
+
- **New in version 0.7.0:** Functions decorated with [`@du.callback`](https://github.com/fohrloop/dash-uploader/blob/dev/docs/dash-uploader.md#ducallback) must take `status` ([`du.UploadStatus`](https://github.com/fohrloop/dash-uploader/blob/dev/docs/dash-uploader.md#duuploadstatus)) object as the only input parameter (previously, `filenames`)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
import uuid
|
|
101
|
+
|
|
102
|
+
import dash_uploader_ng as du
|
|
103
|
+
import dash
|
|
104
|
+
from dash import html
|
|
105
|
+
from dash.dependencies import Input, Output, State
|
|
106
|
+
|
|
107
|
+
app = dash.Dash(__name__)
|
|
108
|
+
|
|
109
|
+
UPLOAD_FOLDER_ROOT = r"C:\tmp\Uploads"
|
|
110
|
+
du.configure_upload(app, UPLOAD_FOLDER_ROOT)
|
|
111
|
+
|
|
112
|
+
def get_upload_component(id):
|
|
113
|
+
return du.Upload(
|
|
114
|
+
id=id,
|
|
115
|
+
max_file_size=1800, # 1800 MB
|
|
116
|
+
filetypes=['csv', 'zip'],
|
|
117
|
+
upload_id=uuid.uuid1(), # Unique session id
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_app_layout():
|
|
122
|
+
|
|
123
|
+
return html.Div(
|
|
124
|
+
[
|
|
125
|
+
html.H1('Demo'),
|
|
126
|
+
html.Div(
|
|
127
|
+
[
|
|
128
|
+
get_upload_component(id='dash-uploader'),
|
|
129
|
+
html.Div(id='callback-output'),
|
|
130
|
+
],
|
|
131
|
+
style={ # wrapper div style
|
|
132
|
+
'textAlign': 'center',
|
|
133
|
+
'width': '600px',
|
|
134
|
+
'padding': '10px',
|
|
135
|
+
'display': 'inline-block'
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
style={
|
|
139
|
+
'textAlign': 'center',
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# get_app_layout is a function
|
|
145
|
+
# This way we can use unique session id's as upload_id's
|
|
146
|
+
app.layout = get_app_layout
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@du.callback(
|
|
150
|
+
output=Output("callback-output", "children"),
|
|
151
|
+
id="dash-uploader",
|
|
152
|
+
)
|
|
153
|
+
def callback_on_completion(status: du.UploadStatus):
|
|
154
|
+
return html.Ul([html.Li(str(x)) for x in status.uploaded_files])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
app.run(debug=True)
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Contributing
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
| What? | How? |
|
|
168
|
+
| :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| 🐞 Found a bug? | 🎟 <a href="https://github.com/fohrloop/dash-uploader/issues">File an Issue</a> |
|
|
170
|
+
| 🙋♂️ Need help? | ❔ <a href="https://stackoverflow.com/questions/ask">Ask a question on StackOverflow</a> <br><a href="https://community.plotly.com/t/show-and-tell-dash-uploader-upload-large-files/38451">📧 Use this thread on community.plotly.com</a> |
|
|
171
|
+
| 💡 Want to submit a feature request? | <a href="https://community.plotly.com/t/show-and-tell-dash-uploader-upload-large-files/38451">🎭 Discuss about it on community.plotly.com</a><br><a href="https://github.com/fohrloop/dash-uploader/issues">🎫 File an Issue (feature request)</a> |
|
|
172
|
+
| 🧙 Want to write code? | 🔥 <a href="./docs/CONTRIBUTING.md">Here's how you get started!</a> |
|
|
173
|
+
## Documentation
|
|
174
|
+
- [Documentation](docs/dash-uploader.md)
|
|
175
|
+
- [Developer documentation](docs/CONTRIBUTING.md)
|
|
176
|
+
- [Migration Guide](https://github.com/fohrloop/dash-uploader/wiki/Migration-Guide)
|
|
177
|
+
|
|
178
|
+
## Changelog
|
|
179
|
+
|
|
180
|
+
- [Changelog](./docs/CHANGELOG.md)
|
|
181
|
+
|
|
182
|
+
## Maintainers
|
|
183
|
+
|
|
184
|
+
- **Awwab Mahdi** — human maintainer. Security reports and issues go to
|
|
185
|
+
[GitHub](https://github.com/obliviance/dash-uploader-ng/security/advisories/new);
|
|
186
|
+
see [SECURITY.md](SECURITY.md).
|
|
187
|
+
- **Claude (Anthropic)** — the CVE-2026-38360 fix, the modernization, and the
|
|
188
|
+
upstream parity test suite in this fork.
|
|
189
|
+
|
|
190
|
+
`dash-uploader-ng` is an independent fork and is **not** affiliated with or
|
|
191
|
+
endorsed by the original author. Original authorship is retained in
|
|
192
|
+
[LICENSE.txt](LICENSE.txt) and in the package's `authors` metadata.
|
|
193
|
+
|
|
194
|
+
## Credits
|
|
195
|
+
- History: This package is based on the React 16 compatible version [dash-resumable-upload](https://github.com/westonkjones/dash-resumable-upload) (0.0.4) by [Weston Jones](https://github.com/westonkjones/) which in turn is based on [dash-resumable-upload](https://github.com/rmarren1/dash-resumable-upload) (0.0.3) by [Ryan Marren](https://github.com/rmarren1)
|
|
196
|
+
- The package boilerplate is taken from the [dash-component-boilerplate](https://github.com/plotly/dash-component-boilerplate).
|
|
197
|
+
- The uploading JS function utilizes the [flow.js](https://github.com/flowjs/flow.js/) (2.14.x).
|
|
198
|
+
- The JS component is created using [React](https://github.com/facebook/react) (17.0.x)
|
|
199
|
+
- The CSS styling is mostly from [Bootstrap](https://getbootstrap.com/) 4.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## CVE-2026-38360 — path traversal → RCE (fixed in this fork)
|
|
4
|
+
|
|
5
|
+
**Affected:** upstream `dash-uploader`, all versions `0.1.0` through `0.7.0a2`.
|
|
6
|
+
**Severity:** Critical — CWE-22, CVSS 3.1 score 9.8 (network / no auth / low complexity).
|
|
7
|
+
**Status upstream:** unpatched; the repository was archived on 2025-07-19.
|
|
8
|
+
**Status here:** fixed in `dash-uploader-ng` from `1.0.0` onward.
|
|
9
|
+
|
|
10
|
+
### What it was
|
|
11
|
+
|
|
12
|
+
The upload endpoint (`/API/dash-uploader` by default) took three form fields
|
|
13
|
+
straight off an unauthenticated `POST` and joined them onto the upload
|
|
14
|
+
directory without checking that the result stayed inside it:
|
|
15
|
+
|
|
16
|
+
- `upload_id` — used as a directory name under the upload root
|
|
17
|
+
- `flowFilename` — used as the saved file's name
|
|
18
|
+
- `flowIdentifier` — used as a per-upload temp directory name
|
|
19
|
+
|
|
20
|
+
A single request was enough to write outside the upload root:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
upload_id=../../../../usr/local/lib/python3.10/site-packages
|
|
24
|
+
flowFilename=evil.pth
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Writing a `.pth` file into `site-packages` turns an arbitrary file write into
|
|
28
|
+
**remote code execution**: Python executes `.pth` files on the next interpreter
|
|
29
|
+
start. Other primitives (dropping `sitecustomize.py`, overwriting a module or a
|
|
30
|
+
WSGI entry point, planting a script in a cron directory) get there too.
|
|
31
|
+
|
|
32
|
+
### How it's fixed
|
|
33
|
+
|
|
34
|
+
Validation happens once, at the request boundary, in
|
|
35
|
+
[`dash_uploader_ng/safepath.py`](dash_uploader_ng/safepath.py), in two layers:
|
|
36
|
+
|
|
37
|
+
1. **Reject bad components up front.** `upload_id` and `flowIdentifier` must be
|
|
38
|
+
a single safe path segment; `flowFilename` / `flowRelativePath` have any
|
|
39
|
+
directory part stripped (folder uploads are flattened, as upstream
|
|
40
|
+
documented) and the remainder validated. A rejected request returns
|
|
41
|
+
**HTTP 400**, not a 500.
|
|
42
|
+
2. **Re-check the resolved path before every write.** `ensure_within()`
|
|
43
|
+
resolves the candidate and the upload root and refuses anything that lands
|
|
44
|
+
outside the root — this also catches escapes that only appear after symlink
|
|
45
|
+
resolution, which a purely lexical check on the form fields cannot see.
|
|
46
|
+
|
|
47
|
+
Filenames are **not** run through `werkzeug.utils.secure_filename`, on purpose:
|
|
48
|
+
it ASCII-folds, so it would silently turn `données.csv` into `donnes.csv` and
|
|
49
|
+
empty out a fully non-Latin filename. The fork strips separators and validates
|
|
50
|
+
the remainder instead, so real-world filenames survive intact.
|
|
51
|
+
|
|
52
|
+
The handler was also hardened against a missing/absurd `flowTotalChunks` and an
|
|
53
|
+
out-of-range `flowChunkNumber`, which previously crashed with a 500.
|
|
54
|
+
|
|
55
|
+
### Tests
|
|
56
|
+
|
|
57
|
+
- [`tests/test_safepath.py`](tests/test_safepath.py) — the validation helpers in
|
|
58
|
+
isolation (accept/reject tables, including a symlink-escape case).
|
|
59
|
+
- [`tests/test_security_cve_2026_38360.py`](tests/test_security_cve_2026_38360.py)
|
|
60
|
+
— the published proof of concept and variants fired at a live Flask app,
|
|
61
|
+
asserting against the filesystem that nothing lands outside the upload root,
|
|
62
|
+
plus regression tests that ordinary and non-ASCII uploads still work.
|
|
63
|
+
|
|
64
|
+
- [`tests/test_upstream_parity.py`](tests/test_upstream_parity.py) —
|
|
65
|
+
**differential tests** that run this fork's handler and the *pristine
|
|
66
|
+
upstream handler* (vendored at `tests/_upstream/`) over the same requests and
|
|
67
|
+
diff the results. Legitimate traffic must behave identically; hostile traffic
|
|
68
|
+
must not.
|
|
69
|
+
|
|
70
|
+
The first three run headless: `pytest` (no browser required). Upstream's
|
|
71
|
+
Selenium suite lives in `tests/browser/` and also passes — see
|
|
72
|
+
[Compatibility](#compatibility-with-dash-uploader) below.
|
|
73
|
+
|
|
74
|
+
## What is actually exploitable, per vector
|
|
75
|
+
|
|
76
|
+
Measured by running the real upstream code, not inferred from the advisory:
|
|
77
|
+
|
|
78
|
+
| Vector | Upstream behaviour | This fork |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `upload_id` traversal | **200 OK, writes outside the upload root** — the RCE primitive | 400, nothing written |
|
|
81
|
+
| `flowIdentifier` traversal | Writes outside the root, but only **observably on an incomplete upload** — a completed one `rmtree()`s the evidence | 400, nothing written |
|
|
82
|
+
| `flowFilename` traversal | **500** — dies on `FileNotFoundError` before it can escape; a denial of service, not an arbitrary write | 200, flattened to a bare filename inside the root |
|
|
83
|
+
|
|
84
|
+
Two things worth knowing here. The `flowIdentifier` vector looks harmless if
|
|
85
|
+
you test it with a single-chunk upload, because the escaped directory is the
|
|
86
|
+
temp chunk directory and upstream deletes it on completion — declare more
|
|
87
|
+
chunks than you send and the escaped file simply stays. And the advisory is
|
|
88
|
+
*more* pessimistic than the code for `flowFilename`: that one crashes rather
|
|
89
|
+
than writing anywhere.
|
|
90
|
+
|
|
91
|
+
## Compatibility with dash-uploader
|
|
92
|
+
|
|
93
|
+
The fork is a drop-in for everything a real flow.js client sends. Verified two
|
|
94
|
+
ways:
|
|
95
|
+
|
|
96
|
+
- **Upstream's own browser suite passes** — 7 passed, 1 skipped (upstream's own
|
|
97
|
+
Windows-only marker), against Chromium in headless mode, including a real
|
|
98
|
+
end-to-end file upload through the browser.
|
|
99
|
+
- **Differential tests** confirm byte-identical results to upstream for
|
|
100
|
+
single-chunk, multi-chunk, re-upload, empty, binary, and non-ASCII
|
|
101
|
+
(`données.csv`, `отчет.csv`, `報告書.xlsx`) uploads, with and without
|
|
102
|
+
`use_upload_id`.
|
|
103
|
+
|
|
104
|
+
### Deliberate behaviour changes
|
|
105
|
+
|
|
106
|
+
These are the cases where the fork does **not** match upstream. All are pinned
|
|
107
|
+
by tests so they cannot drift silently.
|
|
108
|
+
|
|
109
|
+
1. **Stricter `upload_id`.** Values upstream accepted are now rejected with a
|
|
110
|
+
400: spaces (`my session`), colons (`session:42`), non-ASCII (`café`),
|
|
111
|
+
leading dots (`.hidden`), trailing dots (`session.`). uuid1/uuid4 — what
|
|
112
|
+
`du.Upload()` generates by default — are unaffected. If your app passes its
|
|
113
|
+
own `upload_id`, map it to `[A-Za-z0-9._-]` starting with an alphanumeric
|
|
114
|
+
first. The alternative was hand-reasoning about which "harmless" oddities
|
|
115
|
+
compose into a traversal on which filesystem.
|
|
116
|
+
2. **Errors are real HTTP errors.** Upstream logged the exception and returned
|
|
117
|
+
`None`, which Flask rejected as an invalid response — so failures surfaced
|
|
118
|
+
as an accidental 500, and the `abort()` calls inside `_get` were swallowed
|
|
119
|
+
and became 500s too. Malformed requests are now 400, genuine faults 500, and
|
|
120
|
+
`_get`'s 404 is a 404.
|
|
121
|
+
3. **`post_after` / `get_after` run on success only.** A failing request now
|
|
122
|
+
propagates as an HTTP error response instead of being swallowed, so those
|
|
123
|
+
hooks no longer fire on failure. Override `post`/`get` if you need a hook
|
|
124
|
+
that runs either way.
|
|
125
|
+
4. **A path in `flowFilename` is flattened, not fatal.** Upstream 500s; the
|
|
126
|
+
fork strips the directory part and saves the bare filename.
|
|
127
|
+
|
|
128
|
+
## Reporting a vulnerability
|
|
129
|
+
|
|
130
|
+
Report through **GitHub**, which is the channel a human actually monitors:
|
|
131
|
+
|
|
132
|
+
- Preferred, for anything sensitive: open a private security advisory at
|
|
133
|
+
<https://github.com/obliviance/dash-uploader-ng/security/advisories/new>
|
|
134
|
+
- Otherwise: <https://github.com/obliviance/dash-uploader-ng/issues>
|
|
135
|
+
|
|
136
|
+
Please don't route security reports to the package metadata. `Claude
|
|
137
|
+
(Anthropic)` is credited as a maintainer for the work on this fork, but it is
|
|
138
|
+
an AI assistant and cannot receive or triage reports; **Awwab Mahdi** is the
|
|
139
|
+
human maintainer. Nothing sensitive should be sent anywhere but the two links
|
|
140
|
+
above.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os as _os
|
|
2
|
+
import sys as _sys
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import dash as _dash
|
|
6
|
+
|
|
7
|
+
from dash_uploader_ng.configure_upload import configure_upload
|
|
8
|
+
from dash_uploader_ng.callbacks import callback
|
|
9
|
+
from dash_uploader_ng.httprequesthandler import HttpRequestHandler
|
|
10
|
+
from dash_uploader_ng.upload import Upload
|
|
11
|
+
from dash_uploader_ng.uploadstatus import UploadStatus
|
|
12
|
+
|
|
13
|
+
# noinspection PyUnresolvedReferences
|
|
14
|
+
from ._build._imports_ import * # noqa: F403,F401
|
|
15
|
+
from ._build._imports_ import __all__ as build_all
|
|
16
|
+
|
|
17
|
+
# Defines all exposed APIs of this package.
|
|
18
|
+
__all__ = [
|
|
19
|
+
"configure_upload",
|
|
20
|
+
"callback",
|
|
21
|
+
"HttpRequestHandler",
|
|
22
|
+
"Upload",
|
|
23
|
+
"UploadStatus",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
if not hasattr(_dash, "development"):
|
|
27
|
+
print(
|
|
28
|
+
"Dash was not successfully imported. "
|
|
29
|
+
"Make sure you don't have a file "
|
|
30
|
+
'named \n"dash.py" in your current directory.',
|
|
31
|
+
file=_sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
_sys.exit(1)
|
|
34
|
+
|
|
35
|
+
_basepath = _os.path.dirname(__file__)
|
|
36
|
+
_filepath = _os.path.abspath(_os.path.join(_basepath, "_build", "package-info.json"))
|
|
37
|
+
with open(_filepath) as f:
|
|
38
|
+
package = json.load(f)
|
|
39
|
+
|
|
40
|
+
package_name = package["name"].replace(" ", "_").replace("-", "_")
|
|
41
|
+
__version__ = package["version"]
|
|
42
|
+
|
|
43
|
+
_current_path = _os.path.dirname(_os.path.abspath(__file__))
|
|
44
|
+
|
|
45
|
+
_this_module = _sys.modules[__name__]
|
|
46
|
+
_js_dist = [
|
|
47
|
+
{"relative_package_path": "_build/dash_uploader_ng.min.js", "namespace": package_name},
|
|
48
|
+
{
|
|
49
|
+
"relative_package_path": "_build/dash_uploader_ng.min.js.map",
|
|
50
|
+
"namespace": package_name,
|
|
51
|
+
"dynamic": True,
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
_css_dist = []
|
|
56
|
+
|
|
57
|
+
for _component in build_all:
|
|
58
|
+
setattr(locals()[_component], "_js_dist", _js_dist)
|
|
59
|
+
setattr(locals()[_component], "_css_dist", _css_dist)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# AUTO GENERATED FILE - DO NOT EDIT
|
|
2
|
+
|
|
3
|
+
import typing # noqa: F401
|
|
4
|
+
from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401
|
|
5
|
+
from dash.development.base_component import Component, _explicitize_args
|
|
6
|
+
try:
|
|
7
|
+
from dash.types import NumberType # noqa: F401
|
|
8
|
+
except ImportError:
|
|
9
|
+
# Backwards compatibility for dash<=4.1.0
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
raise
|
|
12
|
+
NumberType = typing.Union[ # noqa: F401
|
|
13
|
+
typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
ComponentSingleType = typing.Union[str, int, float, Component, None]
|
|
17
|
+
ComponentType = typing.Union[
|
|
18
|
+
ComponentSingleType,
|
|
19
|
+
typing.Sequence[ComponentSingleType],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Button(Component):
|
|
24
|
+
"""A Button component.
|
|
25
|
+
A Button component.
|
|
26
|
+
Used as a part of Upload component.
|
|
27
|
+
|
|
28
|
+
Keyword arguments:
|
|
29
|
+
|
|
30
|
+
- btnClass (string; default 'dash-uploader-btn'):
|
|
31
|
+
The CSS class for the button.
|
|
32
|
+
|
|
33
|
+
- disabled (boolean; default False):
|
|
34
|
+
Is disabled, the component is not shown.
|
|
35
|
+
|
|
36
|
+
- isUploading (boolean; default False):
|
|
37
|
+
Is True, the parent component has upload in progress.
|
|
38
|
+
|
|
39
|
+
- text (string; default ''):
|
|
40
|
+
The text on the button."""
|
|
41
|
+
_children_props: typing.List[str] = []
|
|
42
|
+
_base_nodes = ['children']
|
|
43
|
+
_namespace = 'dash_uploader_ng'
|
|
44
|
+
_type = 'Button'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
text: typing.Optional[str] = None,
|
|
50
|
+
btnClass: typing.Optional[str] = None,
|
|
51
|
+
onClick: typing.Optional[typing.Any] = None,
|
|
52
|
+
disabled: typing.Optional[bool] = None,
|
|
53
|
+
isUploading: typing.Optional[bool] = None,
|
|
54
|
+
**kwargs
|
|
55
|
+
):
|
|
56
|
+
self._prop_names = ['btnClass', 'disabled', 'isUploading', 'text']
|
|
57
|
+
self._valid_wildcard_attributes = []
|
|
58
|
+
self.available_properties = ['btnClass', 'disabled', 'isUploading', 'text']
|
|
59
|
+
self.available_wildcard_properties = []
|
|
60
|
+
_explicit_args = kwargs.pop('_explicit_args')
|
|
61
|
+
_locals = locals()
|
|
62
|
+
_locals.update(kwargs) # For wildcard attrs and excess named props
|
|
63
|
+
args = {k: _locals[k] for k in _explicit_args}
|
|
64
|
+
|
|
65
|
+
super(Button, self).__init__(**args)
|
|
66
|
+
|
|
67
|
+
setattr(Button, "__init__", _explicitize_args(Button.__init__))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AUTO GENERATED FILE - DO NOT EDIT
|
|
2
|
+
|
|
3
|
+
import typing # noqa: F401
|
|
4
|
+
from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401
|
|
5
|
+
from dash.development.base_component import Component, _explicitize_args
|
|
6
|
+
try:
|
|
7
|
+
from dash.types import NumberType # noqa: F401
|
|
8
|
+
except ImportError:
|
|
9
|
+
# Backwards compatibility for dash<=4.1.0
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
raise
|
|
12
|
+
NumberType = typing.Union[ # noqa: F401
|
|
13
|
+
typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
ComponentSingleType = typing.Union[str, int, float, Component, None]
|
|
17
|
+
ComponentType = typing.Union[
|
|
18
|
+
ComponentSingleType,
|
|
19
|
+
typing.Sequence[ComponentSingleType],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProgressBar(Component):
|
|
24
|
+
"""A ProgressBar component.
|
|
25
|
+
A ProgressBar component.
|
|
26
|
+
Used as a part of Upload component.
|
|
27
|
+
|
|
28
|
+
Keyword arguments:
|
|
29
|
+
|
|
30
|
+
- isUploading (boolean; default False):
|
|
31
|
+
The upload status (boolean).
|
|
32
|
+
|
|
33
|
+
- progressBar (number; default 0):
|
|
34
|
+
The progressbar value."""
|
|
35
|
+
_children_props: typing.List[str] = []
|
|
36
|
+
_base_nodes = ['children']
|
|
37
|
+
_namespace = 'dash_uploader_ng'
|
|
38
|
+
_type = 'ProgressBar'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
progressBar: typing.Optional[NumberType] = None,
|
|
44
|
+
isUploading: typing.Optional[bool] = None,
|
|
45
|
+
**kwargs
|
|
46
|
+
):
|
|
47
|
+
self._prop_names = ['isUploading', 'progressBar']
|
|
48
|
+
self._valid_wildcard_attributes = []
|
|
49
|
+
self.available_properties = ['isUploading', 'progressBar']
|
|
50
|
+
self.available_wildcard_properties = []
|
|
51
|
+
_explicit_args = kwargs.pop('_explicit_args')
|
|
52
|
+
_locals = locals()
|
|
53
|
+
_locals.update(kwargs) # For wildcard attrs and excess named props
|
|
54
|
+
args = {k: _locals[k] for k in _explicit_args}
|
|
55
|
+
|
|
56
|
+
super(ProgressBar, self).__init__(**args)
|
|
57
|
+
|
|
58
|
+
setattr(ProgressBar, "__init__", _explicitize_args(ProgressBar.__init__))
|