Skip to content

Commit 26bf51c

Browse files
Back porting of security patches (#3197)
* Merge pull request from GHSA-fr2w-mp56-g4xp * Enforce file download for attachments table(s) * Enforce file download for attachment in 'StockItemTestResult' table (cherry picked from commit 76aa3a7) * Merge pull request from GHSA-7rq4-qcpw-74gq * Merge pull request from GHSA-rm89-9g65-4ffr * Enable HTML escaping for all tables by default * Enable HTML escaping for all tables by default * Adds automatic escaping for bootstrap tables where custom formatter function is specified - Intercept the row data *before* it is provided to the renderer function - Adds a function for sanitizing nested data structure * Sanitize form data before processing (cherry picked from commit cd418d6) * Increment version number for release * Fix sanitization for array case - was missing a return value
1 parent f9c28ee commit 26bf51c

File tree

12 files changed

+195
-63
lines changed

12 files changed

+195
-63
lines changed

Diff for: InvenTree/InvenTree/admin.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Admin classes"""
2+
3+
from import_export.resources import ModelResource
4+
5+
6+
class InvenTreeResource(ModelResource):
7+
"""Custom subclass of the ModelResource class provided by django-import-export"
8+
9+
Ensures that exported data are escaped to prevent malicious formula injection.
10+
Ref: https://owasp.org/www-community/attacks/CSV_Injection
11+
"""
12+
13+
def export_resource(self, obj):
14+
"""Custom function to override default row export behaviour.
15+
16+
Specifically, strip illegal leading characters to prevent formula injection
17+
"""
18+
row = super().export_resource(obj)
19+
20+
illegal_start_vals = ['@', '=', '+', '-', '@', '\t', '\r', '\n']
21+
22+
for idx, val in enumerate(row):
23+
if type(val) is str:
24+
val = val.strip()
25+
26+
# If the value starts with certain 'suspicious' values, remove it!
27+
while len(val) > 0 and val[0] in illegal_start_vals:
28+
# Remove the first character
29+
val = val[1:]
30+
31+
row[idx] = val
32+
33+
return row

Diff for: InvenTree/InvenTree/static/script/inventree/inventree.js

+37
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
inventreeDocReady,
1414
inventreeLoad,
1515
inventreeSave,
16+
sanitizeData,
1617
*/
1718

1819
function attachClipboard(selector, containerselector, textElement) {
@@ -273,6 +274,42 @@ function loadBrandIcon(element, name) {
273274
}
274275
}
275276

277+
278+
/*
279+
* Function to sanitize a (potentially nested) object.
280+
* Iterates through all levels, and sanitizes each primitive string.
281+
*
282+
* Note that this function effectively provides a "deep copy" of the provided data,
283+
* and the original data structure is unaltered.
284+
*/
285+
function sanitizeData(data) {
286+
if (data == null) {
287+
return null;
288+
} else if (Array.isArray(data)) {
289+
// Handle arrays
290+
var arr = [];
291+
data.forEach(function(val) {
292+
arr.push(sanitizeData(val));
293+
});
294+
295+
return arr;
296+
} else if (typeof(data) === 'object') {
297+
// Handle nested structures
298+
var nested = {};
299+
$.each(data, function(k, v) {
300+
nested[k] = sanitizeData(v);
301+
});
302+
303+
return nested;
304+
} else if (typeof(data) === 'string') {
305+
// Perform string replacement
306+
return data.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;').replace(/`/g, '&#x60;');
307+
} else {
308+
return data;
309+
}
310+
}
311+
312+
276313
// Convenience function to determine if an element exists
277314
$.fn.exists = function() {
278315
return this.length !== 0;

Diff for: InvenTree/InvenTree/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from InvenTree.api_version import INVENTREE_API_VERSION
1313

1414
# InvenTree software version
15-
INVENTREE_SW_VERSION = "0.7.1"
15+
INVENTREE_SW_VERSION = "0.7.2"
1616

1717

1818
def inventreeInstanceName():

Diff for: InvenTree/build/admin.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
from import_export.admin import ImportExportModelAdmin
44
from import_export.fields import Field
5-
from import_export.resources import ModelResource
65
import import_export.widgets as widgets
76

87
from build.models import Build, BuildItem
9-
8+
from InvenTree.admin import InvenTreeResource
109
import part.models
1110

1211

13-
class BuildResource(ModelResource):
14-
"""Class for managing import/export of Build data"""
12+
class BuildResource(InvenTreeResource):
13+
"""Class for managing import/export of Build data."""
1514
# For some reason, we need to specify the fields individually for this ModelResource,
1615
# but we don't for other ones.
1716
# TODO: 2022-05-12 - Need to investigate why this is the case!

Diff for: InvenTree/company/admin.py

+11-17
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
import import_export.widgets as widgets
44
from import_export.admin import ImportExportModelAdmin
55
from import_export.fields import Field
6-
from import_export.resources import ModelResource
76

7+
from InvenTree.admin import InvenTreeResource
88
from part.models import Part
99

1010
from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
1111
ManufacturerPartParameter, SupplierPart,
1212
SupplierPriceBreak)
1313

1414

15-
class CompanyResource(ModelResource):
16-
""" Class for managing Company data import/export """
15+
class CompanyResource(InvenTreeResource):
16+
"""Class for managing Company data import/export."""
1717

1818
class Meta:
1919
model = Company
@@ -34,10 +34,8 @@ class CompanyAdmin(ImportExportModelAdmin):
3434
]
3535

3636

37-
class SupplierPartResource(ModelResource):
38-
"""
39-
Class for managing SupplierPart data import/export
40-
"""
37+
class SupplierPartResource(InvenTreeResource):
38+
"""Class for managing SupplierPart data import/export."""
4139

4240
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
4341

@@ -70,10 +68,8 @@ class SupplierPartAdmin(ImportExportModelAdmin):
7068
autocomplete_fields = ('part', 'supplier', 'manufacturer_part',)
7169

7270

73-
class ManufacturerPartResource(ModelResource):
74-
"""
75-
Class for managing ManufacturerPart data import/export
76-
"""
71+
class ManufacturerPartResource(InvenTreeResource):
72+
"""Class for managing ManufacturerPart data import/export."""
7773

7874
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
7975

@@ -118,10 +114,8 @@ class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin):
118114
autocomplete_fields = ('manufacturer_part',)
119115

120116

121-
class ManufacturerPartParameterResource(ModelResource):
122-
"""
123-
Class for managing ManufacturerPartParameter data import/export
124-
"""
117+
class ManufacturerPartParameterResource(InvenTreeResource):
118+
"""Class for managing ManufacturerPartParameter data import/export."""
125119

126120
class Meta:
127121
model = ManufacturerPartParameter
@@ -148,8 +142,8 @@ class ManufacturerPartParameterAdmin(ImportExportModelAdmin):
148142
autocomplete_fields = ('manufacturer_part',)
149143

150144

151-
class SupplierPriceBreakResource(ModelResource):
152-
""" Class for managing SupplierPriceBreak data import/export """
145+
class SupplierPriceBreakResource(InvenTreeResource):
146+
"""Class for managing SupplierPriceBreak data import/export."""
153147

154148
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart))
155149

Diff for: InvenTree/order/admin.py

+37-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
"""Admin functionality for the 'order' app"""
2+
13
from django.contrib import admin
24

35
import import_export.widgets as widgets
46
from import_export.admin import ImportExportModelAdmin
57
from import_export.fields import Field
6-
from import_export.resources import ModelResource
8+
9+
from InvenTree.admin import InvenTreeResource
710

811
from .models import (PurchaseOrder, PurchaseOrderExtraLine,
912
PurchaseOrderLineItem, SalesOrder, SalesOrderAllocation,
@@ -13,6 +16,7 @@
1316

1417
# region general classes
1518
class GeneralExtraLineAdmin:
19+
"""Admin class template for the 'ExtraLineItem' models"""
1620
list_display = (
1721
'order',
1822
'quantity',
@@ -29,18 +33,21 @@ class GeneralExtraLineAdmin:
2933

3034

3135
class GeneralExtraLineMeta:
36+
"""Metaclass template for the 'ExtraLineItem' models"""
3237
skip_unchanged = True
3338
report_skipped = False
3439
clean_model_instances = True
3540
# endregion
3641

3742

3843
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
44+
"""Inline admin class for the PurchaseOrderLineItem model"""
3945
model = PurchaseOrderLineItem
4046
extra = 0
4147

4248

4349
class PurchaseOrderAdmin(ImportExportModelAdmin):
50+
"""Admin class for the PurchaseOrder model"""
4451

4552
exclude = [
4653
'reference_int',
@@ -68,6 +75,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
6875

6976

7077
class SalesOrderAdmin(ImportExportModelAdmin):
78+
"""Admin class for the SalesOrder model"""
7179

7280
exclude = [
7381
'reference_int',
@@ -90,10 +98,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
9098
autocomplete_fields = ('customer',)
9199

92100

93-
class PurchaseOrderResource(ModelResource):
94-
"""
95-
Class for managing import / export of PurchaseOrder data
96-
"""
101+
class PurchaseOrderResource(InvenTreeResource):
102+
"""Class for managing import / export of PurchaseOrder data."""
97103

98104
# Add number of line items
99105
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
@@ -102,6 +108,7 @@ class PurchaseOrderResource(ModelResource):
102108
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
103109

104110
class Meta:
111+
"""Metaclass"""
105112
model = PurchaseOrder
106113
skip_unchanged = True
107114
clean_model_instances = True
@@ -110,8 +117,8 @@ class Meta:
110117
]
111118

112119

113-
class PurchaseOrderLineItemResource(ModelResource):
114-
""" Class for managing import / export of PurchaseOrderLineItem data """
120+
class PurchaseOrderLineItemResource(InvenTreeResource):
121+
"""Class for managing import / export of PurchaseOrderLineItem data."""
115122

116123
part_name = Field(attribute='part__part__name', readonly=True)
117124

@@ -122,23 +129,24 @@ class PurchaseOrderLineItemResource(ModelResource):
122129
SKU = Field(attribute='part__SKU', readonly=True)
123130

124131
class Meta:
132+
"""Metaclass"""
125133
model = PurchaseOrderLineItem
126134
skip_unchanged = True
127135
report_skipped = False
128136
clean_model_instances = True
129137

130138

131-
class PurchaseOrderExtraLineResource(ModelResource):
132-
""" Class for managing import / export of PurchaseOrderExtraLine data """
139+
class PurchaseOrderExtraLineResource(InvenTreeResource):
140+
"""Class for managing import / export of PurchaseOrderExtraLine data."""
133141

134142
class Meta(GeneralExtraLineMeta):
143+
"""Metaclass options."""
144+
135145
model = PurchaseOrderExtraLine
136146

137147

138-
class SalesOrderResource(ModelResource):
139-
"""
140-
Class for managing import / export of SalesOrder data
141-
"""
148+
class SalesOrderResource(InvenTreeResource):
149+
"""Class for managing import / export of SalesOrder data."""
142150

143151
# Add number of line items
144152
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
@@ -147,6 +155,7 @@ class SalesOrderResource(ModelResource):
147155
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
148156

149157
class Meta:
158+
"""Metaclass options"""
150159
model = SalesOrder
151160
skip_unchanged = True
152161
clean_model_instances = True
@@ -155,10 +164,8 @@ class Meta:
155164
]
156165

157166

158-
class SalesOrderLineItemResource(ModelResource):
159-
"""
160-
Class for managing import / export of SalesOrderLineItem data
161-
"""
167+
class SalesOrderLineItemResource(InvenTreeResource):
168+
"""Class for managing import / export of SalesOrderLineItem data."""
162169

163170
part_name = Field(attribute='part__name', readonly=True)
164171

@@ -169,31 +176,34 @@ class SalesOrderLineItemResource(ModelResource):
169176
fulfilled = Field(attribute='fulfilled_quantity', readonly=True)
170177

171178
def dehydrate_sale_price(self, item):
172-
"""
173-
Return a string value of the 'sale_price' field, rather than the 'Money' object.
179+
"""Return a string value of the 'sale_price' field, rather than the 'Money' object.
180+
174181
Ref: https://github.com/inventree/InvenTree/issues/2207
175182
"""
176-
177183
if item.sale_price:
178184
return str(item.sale_price)
179185
else:
180186
return ''
181187

182188
class Meta:
189+
"""Metaclass options"""
183190
model = SalesOrderLineItem
184191
skip_unchanged = True
185192
report_skipped = False
186193
clean_model_instances = True
187194

188195

189-
class SalesOrderExtraLineResource(ModelResource):
190-
""" Class for managing import / export of SalesOrderExtraLine data """
196+
class SalesOrderExtraLineResource(InvenTreeResource):
197+
"""Class for managing import / export of SalesOrderExtraLine data."""
191198

192199
class Meta(GeneralExtraLineMeta):
200+
"""Metaclass options."""
201+
193202
model = SalesOrderExtraLine
194203

195204

196205
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
206+
"""Admin class for the PurchaseOrderLine model"""
197207

198208
resource_class = PurchaseOrderLineItemResource
199209

@@ -210,11 +220,12 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
210220

211221

212222
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
213-
223+
"""Admin class for the PurchaseOrderExtraLine model"""
214224
resource_class = PurchaseOrderExtraLineResource
215225

216226

217227
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
228+
"""Admin class for the SalesOrderLine model"""
218229

219230
resource_class = SalesOrderLineItemResource
220231

@@ -236,11 +247,12 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
236247

237248

238249
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
239-
250+
"""Admin class for the SalesOrderExtraLine model"""
240251
resource_class = SalesOrderExtraLineResource
241252

242253

243254
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
255+
"""Admin class for the SalesOrderShipment model"""
244256

245257
list_display = [
246258
'order',
@@ -258,6 +270,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):
258270

259271

260272
class SalesOrderAllocationAdmin(ImportExportModelAdmin):
273+
"""Admin class for the SalesOrderAllocation model"""
261274

262275
list_display = (
263276
'line',

0 commit comments

Comments
 (0)