Skip to content

Commit cd613e5

Browse files
[SECUR-105] fix: csv injection vulnerability sanitization #8611
1 parent a8d8165 commit cd613e5

File tree

5 files changed

+46
-11
lines changed

5 files changed

+46
-11
lines changed

apps/api/plane/app/views/workspace/base.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from plane.bgtasks.event_tracking_task import track_event
5050
from plane.utils.url import contains_url
5151
from plane.utils.analytics_events import WORKSPACE_CREATED, WORKSPACE_DELETED
52+
from plane.utils.csv_utils import sanitize_csv_row
5253

5354

5455
class WorkSpaceViewSet(BaseViewSet):
@@ -81,12 +82,14 @@ def get_queryset(self):
8182

8283
def create(self, request):
8384
try:
84-
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value([
85-
{
86-
"key": "DISABLE_WORKSPACE_CREATION",
87-
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
88-
}
89-
])
85+
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
86+
[
87+
{
88+
"key": "DISABLE_WORKSPACE_CREATION",
89+
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
90+
}
91+
]
92+
)
9093

9194
if DISABLE_WORKSPACE_CREATION == "1":
9295
return Response(
@@ -369,7 +372,7 @@ def generate_csv_from_rows(self, rows):
369372
"""Generate CSV buffer from rows."""
370373
csv_buffer = io.StringIO()
371374
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
372-
[writer.writerow(row) for row in rows]
375+
[writer.writerow(sanitize_csv_row(row)) for row in rows]
373376
csv_buffer.seek(0)
374377
return csv_buffer
375378

apps/api/plane/bgtasks/analytic_plot_export.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from plane.utils.analytics_plot import build_graph_plot
2525
from plane.utils.exception_logger import log_exception
2626
from plane.utils.issue_filters import issue_filters
27+
from plane.utils.csv_utils import sanitize_csv_row
2728

2829
row_mapping = {
2930
"state__name": "State",
@@ -180,7 +181,7 @@ def generate_csv_from_rows(rows):
180181
"""Generate CSV buffer from rows."""
181182
csv_buffer = io.StringIO()
182183
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
183-
[writer.writerow(row) for row in rows]
184+
[writer.writerow(sanitize_csv_row(row)) for row in rows]
184185
return csv_buffer
185186

186187

apps/api/plane/utils/csv_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# CSV utility functions for safe export
2+
3+
# Characters that trigger formula evaluation in spreadsheet applications
4+
_CSV_FORMULA_TRIGGERS = frozenset(("=", "+", "-", "@", "\t", "\r", "\n"))
5+
6+
7+
def sanitize_csv_value(value):
8+
"""Sanitize a value for CSV export to prevent formula injection.
9+
10+
Prefixes string values starting with formula-triggering characters
11+
with a single quote so spreadsheet applications treat them as text
12+
instead of evaluating them as formulas.
13+
14+
See: https://owasp.org/www-community/attacks/CSV_Injection
15+
"""
16+
if isinstance(value, str) and value and value[0] in _CSV_FORMULA_TRIGGERS:
17+
return "'" + value
18+
return value
19+
20+
21+
def sanitize_csv_row(row):
22+
"""Sanitize all values in a CSV row."""
23+
return [sanitize_csv_value(v) for v in row]

apps/api/plane/utils/exporters/formatters.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
from openpyxl import Workbook
1111

12+
# Module imports
13+
from plane.utils.csv_utils import sanitize_csv_row
14+
1215

1316
class BaseFormatter:
1417
"""Base class for export formatters."""
@@ -84,7 +87,7 @@ def _create_csv_file(self, data: List[List[str]]) -> str:
8487
buf = io.StringIO()
8588
writer = csv.writer(buf, delimiter=",", quoting=csv.QUOTE_ALL)
8689
for row in data:
87-
writer.writerow(row)
90+
writer.writerow(sanitize_csv_row(row))
8891
buf.seek(0)
8992
return buf.getvalue()
9093

apps/api/plane/utils/porters/formatters.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
from openpyxl import Workbook, load_workbook
1919

2020

21+
# Module imports
22+
from plane.utils.csv_utils import sanitize_csv_row, sanitize_csv_value
23+
24+
2125
class BaseFormatter(ABC):
2226
@abstractmethod
2327
def encode(self, data: List[Dict]) -> Union[str, bytes]:
@@ -128,11 +132,12 @@ def encode(self, data: List[Dict]) -> str:
128132

129133
# Write data rows in the same field order
130134
for row in data:
131-
writer.writerow([row.get(key, "") for key in fieldnames])
135+
writer.writerow(sanitize_csv_row([row.get(key, "") for key in fieldnames]))
132136
else:
133137
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=self.delimiter)
134138
writer.writeheader()
135-
writer.writerows(data)
139+
for row in data:
140+
writer.writerow({k: sanitize_csv_value(row.get(k, "")) for k in fieldnames})
136141

137142
return output.getvalue()
138143

0 commit comments

Comments
 (0)