Export escape fix (#13110)

* Allow numeric values to be exported unescapted

* Updated docs

* Explicit regression tests
This commit is contained in:
Oliver authored and GitHub committed 2026-10-27 11:55:24 +11:00
1 parent 8075ef58d9
commit 6c65d96ece
3 files changed
+65 -2

No files matched your search

+11 -2
View File
@@ -515,11 +515,20 @@ def escape_export_value(value):
String values which start with a character that a spreadsheet application
may interpret as a formula are prefixed with a single quote character.
Non-string values are returned unchanged.
Non-string values are returned unchanged, as are strictly numeric
strings (e.g. "-222", "+1.5", "12e6"), which are not a formula risk.
Ref: https://owasp.org/www-community/attacks/CSV_Injection
"""
if isinstance(value, str) and value.startswith(('=', '+', '-', '@', '\t', '\r')):
if not isinstance(value, str):
return value
if value.startswith(('+', '-')) and re.fullmatch(
r'[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?', value
):
return value
if value.startswith(('=', '+', '-', '@', '\t', '\r')):
return "'" + value
return value
@@ -1,7 +1,9 @@
"""Unit test for the exporter plugins."""
from django.test import TestCase
from django.urls import reverse
from InvenTree.helpers import escape_export_value
from InvenTree.unit_test import InvenTreeAPITestCase
from plugin.registry import registry
@@ -152,3 +154,38 @@ class FormulaInjectionExportTest(InvenTreeAPITestCase):
names.add(cell.value)
self.assertEqual(names, set(self.NAMES.values()))
class EscapeExportValueTest(TestCase):
"""Unit tests for the escape_export_value helper."""
def test_numeric_unchanged(self):
"""Strictly numeric values are not escaped, even with a +/- prefix."""
for value in ['123', '-222', '+5', '123.4', '-123.4', '+.5', '12e6', '-1.5E-3']:
self.assertEqual(escape_export_value(value), value)
def test_non_numeric_escaped(self):
"""Non-numeric values with a dangerous prefix are escaped."""
for value in [
'=2+2',
'+1+1',
'-1-1',
'-12V Regulator',
'-',
'+',
'-1e',
'-nan',
'-inf',
'-1 ',
'- 1',
'-1,2',
'@SUM(1)',
'\tabc',
'\rabc',
]:
self.assertEqual(escape_export_value(value), "'" + value)
def test_other_values(self):
"""Safe strings and non-string values are returned unchanged."""
for value in ['Safe Part', '', 'a-b', 42, -42, 1.5, None, True]:
self.assertEqual(escape_export_value(value), value)