Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions apps/api/plane/app/views/workspace/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from plane.bgtasks.event_tracking_task import track_event
from plane.utils.url import contains_url
from plane.utils.analytics_events import WORKSPACE_CREATED, WORKSPACE_DELETED
from plane.utils.csv_utils import sanitize_csv_row


class WorkSpaceViewSet(BaseViewSet):
Expand Down Expand Up @@ -81,12 +82,14 @@ def get_queryset(self):

def create(self, request):
try:
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value([
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
}
])
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
[
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
}
]
)

if DISABLE_WORKSPACE_CREATION == "1":
return Response(
Expand Down Expand Up @@ -369,7 +372,7 @@ def generate_csv_from_rows(self, rows):
"""Generate CSV buffer from rows."""
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
[writer.writerow(row) for row in rows]
[writer.writerow(sanitize_csv_row(row)) for row in rows]
csv_buffer.seek(0)
return csv_buffer

Expand Down
3 changes: 2 additions & 1 deletion apps/api/plane/bgtasks/analytic_plot_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.exception_logger import log_exception
from plane.utils.issue_filters import issue_filters
from plane.utils.csv_utils import sanitize_csv_row

row_mapping = {
"state__name": "State",
Expand Down Expand Up @@ -180,7 +181,7 @@ def generate_csv_from_rows(rows):
"""Generate CSV buffer from rows."""
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
[writer.writerow(row) for row in rows]
[writer.writerow(sanitize_csv_row(row)) for row in rows]
return csv_buffer


Expand Down
23 changes: 23 additions & 0 deletions apps/api/plane/utils/csv_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# CSV utility functions for safe export

# Characters that trigger formula evaluation in spreadsheet applications
_CSV_FORMULA_TRIGGERS = frozenset(("=", "+", "-", "@", "\t", "\r", "\n"))
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add the standard license header to satisfy the addlicense check.

The pipeline is failing the copyright check; this file is missing the standard header used across the repo.

🧾 Proposed fix
+# Copyright (c) 2023-present Plane Software, Inc. and contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+# See the LICENSE file for details.
+
 # CSV utility functions for safe export
🧰 Tools
🪛 GitHub Actions: Copy Right Check

[error] 1-1: Copyright check failed. The command 'addlicense -check -f COPYRIGHT.txt -ignore "/migrations/" $(git ls-files '*.py')' exited with code 1.

🤖 Prompt for AI Agents
In `@apps/api/plane/utils/csv_utils.py` around lines 1 - 4, This file is missing
the standard repository license header; add the project's canonical license
comment block at the very top of the module (above the existing module
docstring/comment and before _CSV_FORMULA_TRIGGERS) so the addlicense check
passes; copy the exact header used in other files and ensure it precedes the
existing content (refer to this module's top-level symbol _CSV_FORMULA_TRIGGERS
to locate the file and verify header placement).



def sanitize_csv_value(value):
"""Sanitize a value for CSV export to prevent formula injection.

Prefixes string values starting with formula-triggering characters
with a single quote so spreadsheet applications treat them as text
instead of evaluating them as formulas.

See: https://owasp.org/www-community/attacks/CSV_Injection
"""
if isinstance(value, str) and value and value[0] in _CSV_FORMULA_TRIGGERS:
return "'" + value
return value


def sanitize_csv_row(row):
"""Sanitize all values in a CSV row."""
return [sanitize_csv_value(v) for v in row]
5 changes: 4 additions & 1 deletion apps/api/plane/utils/exporters/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from openpyxl import Workbook

# Module imports
from plane.utils.csv_utils import sanitize_csv_row


class BaseFormatter:
"""Base class for export formatters."""
Expand Down Expand Up @@ -84,7 +87,7 @@ def _create_csv_file(self, data: List[List[str]]) -> str:
buf = io.StringIO()
writer = csv.writer(buf, delimiter=",", quoting=csv.QUOTE_ALL)
for row in data:
writer.writerow(row)
writer.writerow(sanitize_csv_row(row))
buf.seek(0)
return buf.getvalue()

Expand Down
9 changes: 7 additions & 2 deletions apps/api/plane/utils/porters/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from openpyxl import Workbook, load_workbook


# Module imports
from plane.utils.csv_utils import sanitize_csv_row, sanitize_csv_value


class BaseFormatter(ABC):
@abstractmethod
def encode(self, data: List[Dict]) -> Union[str, bytes]:
Expand Down Expand Up @@ -128,11 +132,12 @@ def encode(self, data: List[Dict]) -> str:

# Write data rows in the same field order
for row in data:
writer.writerow([row.get(key, "") for key in fieldnames])
writer.writerow(sanitize_csv_row([row.get(key, "") for key in fieldnames]))
else:
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=self.delimiter)
writer.writeheader()
writer.writerows(data)
for row in data:
writer.writerow({k: sanitize_csv_value(row.get(k, "")) for k in fieldnames})

return output.getvalue()

Expand Down
Loading