Skip to content

Commit

Permalink
Back porting of security patches (#3197)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SchrodingersGat committed Jun 15, 2022
1 parent f9c28ee commit 26bf51c
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 63 deletions.
33 changes: 33 additions & 0 deletions InvenTree/InvenTree/admin.py
@@ -0,0 +1,33 @@
"""Admin classes"""

from import_export.resources import ModelResource


class InvenTreeResource(ModelResource):
"""Custom subclass of the ModelResource class provided by django-import-export"
Ensures that exported data are escaped to prevent malicious formula injection.
Ref: https://owasp.org/www-community/attacks/CSV_Injection
"""

def export_resource(self, obj):
"""Custom function to override default row export behaviour.
Specifically, strip illegal leading characters to prevent formula injection
"""
row = super().export_resource(obj)

illegal_start_vals = ['@', '=', '+', '-', '@', '\t', '\r', '\n']

for idx, val in enumerate(row):
if type(val) is str:
val = val.strip()

# If the value starts with certain 'suspicious' values, remove it!
while len(val) > 0 and val[0] in illegal_start_vals:
# Remove the first character
val = val[1:]

row[idx] = val

return row
37 changes: 37 additions & 0 deletions InvenTree/InvenTree/static/script/inventree/inventree.js
Expand Up @@ -13,6 +13,7 @@
inventreeDocReady,
inventreeLoad,
inventreeSave,
sanitizeData,
*/

function attachClipboard(selector, containerselector, textElement) {
Expand Down Expand Up @@ -273,6 +274,42 @@ function loadBrandIcon(element, name) {
}
}


/*
* Function to sanitize a (potentially nested) object.
* Iterates through all levels, and sanitizes each primitive string.
*
* Note that this function effectively provides a "deep copy" of the provided data,
* and the original data structure is unaltered.
*/
function sanitizeData(data) {
if (data == null) {
return null;
} else if (Array.isArray(data)) {
// Handle arrays
var arr = [];
data.forEach(function(val) {
arr.push(sanitizeData(val));
});

return arr;
} else if (typeof(data) === 'object') {
// Handle nested structures
var nested = {};
$.each(data, function(k, v) {
nested[k] = sanitizeData(v);
});

return nested;
} else if (typeof(data) === 'string') {
// Perform string replacement
return data.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;').replace(/`/g, '&#x60;');
} else {
return data;
}
}


// Convenience function to determine if an element exists
$.fn.exists = function() {
return this.length !== 0;
Expand Down
2 changes: 1 addition & 1 deletion InvenTree/InvenTree/version.py
Expand Up @@ -12,7 +12,7 @@
from InvenTree.api_version import INVENTREE_API_VERSION

# InvenTree software version
INVENTREE_SW_VERSION = "0.7.1"
INVENTREE_SW_VERSION = "0.7.2"


def inventreeInstanceName():
Expand Down
7 changes: 3 additions & 4 deletions InvenTree/build/admin.py
Expand Up @@ -2,16 +2,15 @@

from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets

from build.models import Build, BuildItem

from InvenTree.admin import InvenTreeResource
import part.models


class BuildResource(ModelResource):
"""Class for managing import/export of Build data"""
class BuildResource(InvenTreeResource):
"""Class for managing import/export of Build data."""
# For some reason, we need to specify the fields individually for this ModelResource,
# but we don't for other ones.
# TODO: 2022-05-12 - Need to investigate why this is the case!
Expand Down
28 changes: 11 additions & 17 deletions InvenTree/company/admin.py
Expand Up @@ -3,17 +3,17 @@
import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource

from InvenTree.admin import InvenTreeResource
from part.models import Part

from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
ManufacturerPartParameter, SupplierPart,
SupplierPriceBreak)


class CompanyResource(ModelResource):
""" Class for managing Company data import/export """
class CompanyResource(InvenTreeResource):
"""Class for managing Company data import/export."""

class Meta:
model = Company
Expand All @@ -34,10 +34,8 @@ class CompanyAdmin(ImportExportModelAdmin):
]


class SupplierPartResource(ModelResource):
"""
Class for managing SupplierPart data import/export
"""
class SupplierPartResource(InvenTreeResource):
"""Class for managing SupplierPart data import/export."""

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

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


class ManufacturerPartResource(ModelResource):
"""
Class for managing ManufacturerPart data import/export
"""
class ManufacturerPartResource(InvenTreeResource):
"""Class for managing ManufacturerPart data import/export."""

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

Expand Down Expand Up @@ -118,10 +114,8 @@ class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin):
autocomplete_fields = ('manufacturer_part',)


class ManufacturerPartParameterResource(ModelResource):
"""
Class for managing ManufacturerPartParameter data import/export
"""
class ManufacturerPartParameterResource(InvenTreeResource):
"""Class for managing ManufacturerPartParameter data import/export."""

class Meta:
model = ManufacturerPartParameter
Expand All @@ -148,8 +142,8 @@ class ManufacturerPartParameterAdmin(ImportExportModelAdmin):
autocomplete_fields = ('manufacturer_part',)


class SupplierPriceBreakResource(ModelResource):
""" Class for managing SupplierPriceBreak data import/export """
class SupplierPriceBreakResource(InvenTreeResource):
"""Class for managing SupplierPriceBreak data import/export."""

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

Expand Down
61 changes: 37 additions & 24 deletions InvenTree/order/admin.py
@@ -1,9 +1,12 @@
"""Admin functionality for the 'order' app"""

from django.contrib import admin

import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource

from InvenTree.admin import InvenTreeResource

from .models import (PurchaseOrder, PurchaseOrderExtraLine,
PurchaseOrderLineItem, SalesOrder, SalesOrderAllocation,
Expand All @@ -13,6 +16,7 @@

# region general classes
class GeneralExtraLineAdmin:
"""Admin class template for the 'ExtraLineItem' models"""
list_display = (
'order',
'quantity',
Expand All @@ -29,18 +33,21 @@ class GeneralExtraLineAdmin:


class GeneralExtraLineMeta:
"""Metaclass template for the 'ExtraLineItem' models"""
skip_unchanged = True
report_skipped = False
clean_model_instances = True
# endregion


class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
"""Inline admin class for the PurchaseOrderLineItem model"""
model = PurchaseOrderLineItem
extra = 0


class PurchaseOrderAdmin(ImportExportModelAdmin):
"""Admin class for the PurchaseOrder model"""

exclude = [
'reference_int',
Expand Down Expand Up @@ -68,6 +75,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):


class SalesOrderAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrder model"""

exclude = [
'reference_int',
Expand All @@ -90,10 +98,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
autocomplete_fields = ('customer',)


class PurchaseOrderResource(ModelResource):
"""
Class for managing import / export of PurchaseOrder data
"""
class PurchaseOrderResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrder data."""

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

class Meta:
"""Metaclass"""
model = PurchaseOrder
skip_unchanged = True
clean_model_instances = True
Expand All @@ -110,8 +117,8 @@ class Meta:
]


class PurchaseOrderLineItemResource(ModelResource):
""" Class for managing import / export of PurchaseOrderLineItem data """
class PurchaseOrderLineItemResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrderLineItem data."""

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

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

class Meta:
"""Metaclass"""
model = PurchaseOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True


class PurchaseOrderExtraLineResource(ModelResource):
""" Class for managing import / export of PurchaseOrderExtraLine data """
class PurchaseOrderExtraLineResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrderExtraLine data."""

class Meta(GeneralExtraLineMeta):
"""Metaclass options."""

model = PurchaseOrderExtraLine


class SalesOrderResource(ModelResource):
"""
Class for managing import / export of SalesOrder data
"""
class SalesOrderResource(InvenTreeResource):
"""Class for managing import / export of SalesOrder data."""

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

class Meta:
"""Metaclass options"""
model = SalesOrder
skip_unchanged = True
clean_model_instances = True
Expand All @@ -155,10 +164,8 @@ class Meta:
]


class SalesOrderLineItemResource(ModelResource):
"""
Class for managing import / export of SalesOrderLineItem data
"""
class SalesOrderLineItemResource(InvenTreeResource):
"""Class for managing import / export of SalesOrderLineItem data."""

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

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

def dehydrate_sale_price(self, item):
"""
Return a string value of the 'sale_price' field, rather than the 'Money' object.
"""Return a string value of the 'sale_price' field, rather than the 'Money' object.
Ref: https://github.com/inventree/InvenTree/issues/2207
"""

if item.sale_price:
return str(item.sale_price)
else:
return ''

class Meta:
"""Metaclass options"""
model = SalesOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True


class SalesOrderExtraLineResource(ModelResource):
""" Class for managing import / export of SalesOrderExtraLine data """
class SalesOrderExtraLineResource(InvenTreeResource):
"""Class for managing import / export of SalesOrderExtraLine data."""

class Meta(GeneralExtraLineMeta):
"""Metaclass options."""

model = SalesOrderExtraLine


class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
"""Admin class for the PurchaseOrderLine model"""

resource_class = PurchaseOrderLineItemResource

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


class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):

"""Admin class for the PurchaseOrderExtraLine model"""
resource_class = PurchaseOrderExtraLineResource


class SalesOrderLineItemAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderLine model"""

resource_class = SalesOrderLineItemResource

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


class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):

"""Admin class for the SalesOrderExtraLine model"""
resource_class = SalesOrderExtraLineResource


class SalesOrderShipmentAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderShipment model"""

list_display = [
'order',
Expand All @@ -258,6 +270,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):


class SalesOrderAllocationAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderAllocation model"""

list_display = (
'line',
Expand Down

0 comments on commit 26bf51c

Please sign in to comment.