VYPR
High severityNVD Advisory· Published Jun 17, 2022· Updated Aug 3, 2024

Unrestricted Upload of File with Dangerous Type in inventree/inventree

CVE-2022-2111

Description

Unrestricted Upload of File with Dangerous Type in GitHub repository inventree/inventree prior to 0.7.2.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

InvenTree prior to 0.7.2 allowed authenticated users to upload arbitrary files as attachments, enabling stored XSS when other users open them in the browser.

Vulnerability

CVE-2022-2111 is an unrestricted file upload vulnerability in InvenTree, an open-source inventory management system. The application allowed authenticated users to upload files with dangerous types (e.g., HTML files containing JavaScript) as attachments to database records. This lack of validation on file content and extension meant that any file could be uploaded without restriction [1][3].

Exploitation

An attacker with a valid user account could upload a malicious HTML file containing JavaScript code. When another user views the attachment in their browser (e.g., by clicking the link), the malicious script executes in the context of the InvenTree application. No additional privileges beyond a standard user account are required to perform the upload [3].

Impact

Successful exploitation leads to stored cross-site scripting (XSS). An attacker can execute arbitrary JavaScript in the victim's browser, potentially stealing session cookies, performing actions on behalf of the victim, or defacing the application. The impact is limited to users who open the malicious attachment in the browser [3].

Mitigation

The issue is patched in InvenTree version 0.7.2 (backported to the 0.7.x branch) and in the upcoming 0.8.0 release. Users are advised to upgrade immediately. As a workaround, users can right-click attachment links and select "Save link as" to download the file before opening it, which prevents automatic execution in the browser context [1][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
inventreePyPI
< 0.7.20.7.2

Affected products

1

Patches

1
26bf51c20a1c

Back porting of security patches (#3197)

https://github.com/inventree/inventreeOliverJun 15, 2022via ghsa
12 files changed · +195 63
  • InvenTree/build/admin.py+3 4 modified
    @@ -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!
    
  • InvenTree/company/admin.py+11 17 modified
    @@ -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
    @@ -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))
     
    @@ -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))
     
    @@ -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
    @@ -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))
     
    
  • InvenTree/InvenTree/admin.py+33 0 added
    @@ -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
    
  • InvenTree/InvenTree/static/script/inventree/inventree.js+37 0 modified
    @@ -13,6 +13,7 @@
         inventreeDocReady,
         inventreeLoad,
         inventreeSave,
    +    sanitizeData,
     */
     
     function attachClipboard(selector, containerselector, textElement) {
    @@ -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;
    
  • InvenTree/InvenTree/version.py+1 1 modified
    @@ -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():
    
  • InvenTree/order/admin.py+37 24 modified
    @@ -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,
    @@ -13,6 +16,7 @@
     
     # region general classes
     class GeneralExtraLineAdmin:
    +    """Admin class template for the 'ExtraLineItem' models"""
         list_display = (
             'order',
             'quantity',
    @@ -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',
    @@ -68,6 +75,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
     
     
     class SalesOrderAdmin(ImportExportModelAdmin):
    +    """Admin class for the SalesOrder model"""
     
         exclude = [
             'reference_int',
    @@ -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)
    @@ -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
    @@ -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)
     
    @@ -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)
    @@ -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
    @@ -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)
     
    @@ -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
     
    @@ -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
     
    @@ -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',
    @@ -258,6 +270,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):
     
     
     class SalesOrderAllocationAdmin(ImportExportModelAdmin):
    +    """Admin class for the SalesOrderAllocation model"""
     
         list_display = (
             'line',
    
  • InvenTree/part/admin.py+9 9 modified
    @@ -3,15 +3,15 @@
     import import_export.widgets as widgets
     from import_export.admin import ImportExportModelAdmin
     from import_export.fields import Field
    -from import_export.resources import ModelResource
     
     import part.models as models
     from company.models import SupplierPart
    +from InvenTree.admin import InvenTreeResource
     from stock.models import StockLocation
     
     
    -class PartResource(ModelResource):
    -    """ Class for managing Part data import/export """
    +class PartResource(InvenTreeResource):
    +    """Class for managing Part data import/export."""
     
         # ForeignKey fields
         category = Field(attribute='category', widget=widgets.ForeignKeyWidget(models.PartCategory))
    @@ -81,8 +81,8 @@ class PartAdmin(ImportExportModelAdmin):
         ]
     
     
    -class PartCategoryResource(ModelResource):
    -    """ Class for managing PartCategory data import/export """
    +class PartCategoryResource(InvenTreeResource):
    +    """Class for managing PartCategory data import/export."""
     
         parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(models.PartCategory))
     
    @@ -157,8 +157,8 @@ class PartTestTemplateAdmin(admin.ModelAdmin):
         autocomplete_fields = ('part',)
     
     
    -class BomItemResource(ModelResource):
    -    """ Class for managing BomItem data import/export """
    +class BomItemResource(InvenTreeResource):
    +    """Class for managing BomItem data import/export."""
     
         level = Field(attribute='level', readonly=True)
     
    @@ -269,8 +269,8 @@ class ParameterTemplateAdmin(ImportExportModelAdmin):
         search_fields = ('name', 'units')
     
     
    -class ParameterResource(ModelResource):
    -    """ Class for managing PartParameter data import/export """
    +class ParameterResource(InvenTreeResource):
    +    """Class for managing PartParameter data import/export."""
     
         part = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part))
     
    
  • InvenTree/stock/admin.py+5 5 modified
    @@ -3,19 +3,19 @@
     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 build.models import Build
     from company.models import Company, SupplierPart
    +from InvenTree.admin import InvenTreeResource
     from order.models import PurchaseOrder, SalesOrder
     from part.models import Part
     
     from .models import (StockItem, StockItemAttachment, StockItemTestResult,
                          StockItemTracking, StockLocation)
     
     
    -class LocationResource(ModelResource):
    -    """ Class for managing StockLocation data import/export """
    +class LocationResource(InvenTreeResource):
    +    """Class for managing StockLocation data import/export."""
     
         parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
     
    @@ -65,8 +65,8 @@ class LocationAdmin(ImportExportModelAdmin):
         ]
     
     
    -class StockItemResource(ModelResource):
    -    """ Class for managing StockItem data import/export """
    +class StockItemResource(InvenTreeResource):
    +    """Class for managing StockItem data import/export."""
     
         # Custom managers for ForeignKey fields
         part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
    
  • InvenTree/templates/js/translated/attachment.js+1 1 modified
    @@ -149,7 +149,7 @@ function loadAttachmentTable(url, options) {
     
                             var html = `<span class='fas ${icon}'></span> ${filename}`;
     
    -                        return renderLink(html, value);
    +                        return renderLink(html, value, {download: true});
                         } else if (row.link) {
                             var html = `<span class='fas fa-link'></span> ${row.link}`;
                             return renderLink(html, row.link);
    
  • InvenTree/templates/js/translated/forms.js+3 0 modified
    @@ -204,6 +204,9 @@ function constructChangeForm(fields, options) {
             },
             success: function(data) {
     
    +            // Ensure the data are fully sanitized before we operate on it
    +            data = sanitizeData(data);
    +
                 // An optional function can be provided to process the returned results,
                 // before they are rendered to the form
                 if (options.processResults) {
    
  • InvenTree/templates/js/translated/stock.js+2 1 modified
    @@ -1306,7 +1306,8 @@ function loadStockTestResultsTable(table, options) {
                         var html = value;
     
                         if (row.attachment) {
    -                        html += `<a href='${row.attachment}'><span class='fas fa-file-alt float-right'></span></a>`;
    +                        var text = `<span class='fas fa-file-alt float-right'></span>`;
    +                        html += renderLink(text, row.attachment, {download: true});
                         }
     
                         return html;
    
  • InvenTree/templates/js/translated/tables.js+53 1 modified
    @@ -92,6 +92,13 @@ function renderLink(text, url, options={}) {
     
         var max_length = options.max_length || -1;
     
    +    var extra = '';
    +
    +    if (options.download) {
    +        var fn = url.split('/').at(-1);
    +        extra += ` download='${fn}'`;
    +    }
    +
         // Shorten the displayed length if required
         if ((max_length > 0) && (text.length > max_length)) {
             var slice_length = (max_length - 3) / 2;
    @@ -102,7 +109,7 @@ function renderLink(text, url, options={}) {
             text = `${text_start}...${text_end}`;
         }
     
    -    return '<a href="' + url + '">' + text + '</a>';
    +    return `<a href='${url}'${extra}>${text}</a>`;
     }
     
     
    @@ -282,6 +289,8 @@ $.fn.inventreeTable = function(options) {
         // Extract query params
         var filters = options.queryParams || options.filters || {};
     
    +    options.escape = true;
    +
         // Store the total set of query params
         options.query_params = filters;
     
    @@ -468,6 +477,49 @@ function customGroupSorter(sortName, sortOrder, sortData) {
     
         $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['en-US-custom']);
     
    +    // Enable HTML escaping by default
    +    $.fn.bootstrapTable.escape = true;
    +
    +    // Override the 'calculateObjectValue' function at bootstrap-table.js:3525
    +    // Allows us to escape any nasty HTML tags which are rendered to the DOM
    +    $.fn.bootstrapTable.utils._calculateObjectValue = $.fn.bootstrapTable.utils.calculateObjectValue;
    +
    +    $.fn.bootstrapTable.utils.calculateObjectValue = function escapeCellValue(self, name, args, defaultValue) {
    +
    +        var args_list = [];
    +
    +        if (args) {
    +
    +            args_list.push(args[0]);
    +
    +            if (name && typeof(name) === 'function' && name.name == 'formatter') {
    +                /* This is a custom "formatter" function for a particular cell,
    +                * which may side-step regular HTML escaping, and inject malicious code into the DOM.
    +                *
    +                * Here we have access to the 'args' supplied to the custom 'formatter' function,
    +                * which are in the order:
    +                * args = [value, row, index, field]
    +                *
    +                * 'row' is the one we are interested in
    +                */
    +
    +                var row = Object.assign({}, args[1]);
    +
    +                args_list.push(sanitizeData(row));
    +            } else {
    +                args_list.push(args[1]);
    +            }
    +
    +            for (var ii = 2; ii < args.length; ii++) {
    +                args_list.push(args[ii]);
    +            }
    +        }
    +
    +        var value = $.fn.bootstrapTable.utils._calculateObjectValue(self, name, args_list, defaultValue);
    +
    +        return value;
    +    };
    +
     })(jQuery);
     
     $.extend($.fn.treegrid.defaults, {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.