Skip to content

Fix adversarial review findings for CSV export#794

Merged
Chris0Jeky merged 1 commit intofeature/analytics-export-reports-78from
fix/adversarial-review-787-round2
Apr 8, 2026
Merged

Fix adversarial review findings for CSV export#794
Chris0Jeky merged 1 commit intofeature/analytics-export-reports-78from
fix/adversarial-review-787-round2

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • Fix CSV injection via embedded newlines in SanitizeCsvField (strips dangerous leading chars from all lines, not just the first)
  • Forward CancellationToken from controller to export service
  • Replace inefficient .Concat().ToArray() BOM prepend with direct array copy
  • Surface export errors in frontend via toast notification instead of silent catch
  • Add 5 new test cases for embedded-newline injection vectors

Context

Fixes findings from adversarial review of PR #787 (comment: #787 (comment))

Test plan

  • All 34 MetricsExport tests pass (26 unit + 8 integration)
  • Full backend test suite passes (all projects, 0 failures)

…urface export errors in UI

- SanitizeCsvField now strips dangerous leading chars from each embedded
  line, not just the first line, preventing formula injection via
  multi-line cell values (e.g. "hello\n=CMD|'/C calc'!A0")
- ExportBoardMetrics controller action forwards CancellationToken from
  HttpContext.RequestAborted to the export service
- Replace inefficient .Concat().ToArray() BOM prepend with direct array copy
- Frontend exportCsv() now shows toast error on failure instead of
  silently swallowing exceptions
- Add 5 new test cases for embedded-newline CSV injection vectors
Copilot AI review requested due to automatic review settings April 8, 2026 02:01
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky Chris0Jeky merged commit 1617980 into feature/analytics-export-reports-78 Apr 8, 2026
10 checks passed
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Apr 8, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several improvements to the metrics export functionality, including the addition of cancellation token support in the API, a more efficient byte array construction for CSV files with a UTF-8 BOM, and enhanced CSV injection protection that sanitizes embedded newlines. Additionally, the frontend now includes explicit error handling and toast notifications for failed exports. A security concern was identified in the new sanitization logic where bare carriage returns (\r) are not correctly handled as line separators, potentially allowing formula injection in certain spreadsheet applications.

Comment on lines +154 to +169
private static string SanitizeEmbeddedLines(string value)
{
if (!value.Contains('\n') && !value.Contains('\r'))
return value;

var lines = value.Split('\n');
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
// Handle \r\n: the \r will be at the end of the previous line's split,
// but also strip dangerous chars that appear after a bare \n.
lines[i] = StripDangerousLeadingChars(line);
}

return string.Join('\n', lines);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The SanitizeEmbeddedLines method currently only splits the input by \n. While this correctly handles \n and \r\n (as the \r remains at the end of the previous segment), it fails to sanitize lines separated by a bare carriage return (\r). Since \r is explicitly listed as a dangerous character in IsDangerousLeadingChar, it should also be treated as a potential line separator to prevent formula injection in spreadsheet applications that recognize it as such. Normalizing all line endings to \n during this process is a robust and safe approach for CSV fields.

    private static string SanitizeEmbeddedLines(string value)
    {
        if (!value.Contains('\n') && !value.Contains('\r'))
            return value;

        // Normalize line endings to \n to ensure all embedded lines are caught,
        // then strip dangerous characters from the start of each line (except the first,
        // which is handled by the caller).
        var lines = value.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
        for (var i = 1; i < lines.Length; i++)
        {
            lines[i] = StripDangerousLeadingChars(lines[i]);
        }

        return string.Join('\n', lines);
    }

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Addresses adversarial review findings in the board-metrics CSV export by tightening CSV-injection sanitization, improving request cancellation propagation, optimizing UTF-8 BOM handling, and surfacing export failures in the UI.

Changes:

  • Harden SanitizeCsvField against embedded-newline CSV injection and add new test cases.
  • Forward CancellationToken from the API controller into the export service.
  • Optimize UTF-8 BOM prepending and show export errors via frontend toast notifications.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
frontend/taskdeck-web/src/views/MetricsView.vue Adds toast-based error surfacing for failed CSV export requests.
backend/tests/Taskdeck.Application.Tests/Services/MetricsExportServiceTests.cs Adds theory cases covering embedded-newline injection vectors in CSV sanitization.
backend/src/Taskdeck.Application/Services/MetricsExportService.cs Improves CSV field sanitization for embedded lines and replaces BOM .Concat().ToArray() with direct array copy.
backend/src/Taskdeck.Api/Controllers/MetricsController.cs Threads request cancellation token through to the export service call.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +159 to +168
var lines = value.Split('\n');
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
// Handle \r\n: the \r will be at the end of the previous line's split,
// but also strip dangerous chars that appear after a bare \n.
lines[i] = StripDangerousLeadingChars(line);
}

return string.Join('\n', lines);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

SanitizeEmbeddedLines() checks for both '\n' and '\r' but only splits on '\n'. If an attacker uses a bare carriage return as the embedded line break (e.g. "safe\r=CMD(...)"), the second line is never sanitized and the formula prefix can remain. Consider sanitizing after any newline boundary (handle '\r' as a delimiter too, including '\r\n'), and add a unit test covering the '\r' (no '\n') case.

Suggested change
var lines = value.Split('\n');
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
// Handle \r\n: the \r will be at the end of the previous line's split,
// but also strip dangerous chars that appear after a bare \n.
lines[i] = StripDangerousLeadingChars(line);
}
return string.Join('\n', lines);
var result = new StringBuilder(value.Length);
var segmentStart = 0;
var isFirstLine = true;
for (var i = 0; i < value.Length; i++)
{
if (value[i] != '\n' && value[i] != '\r')
continue;
var segment = value[segmentStart..i];
result.Append(isFirstLine ? segment : StripDangerousLeadingChars(segment));
if (value[i] == '\r' && i + 1 < value.Length && value[i + 1] == '\n')
{
result.Append("\r\n");
i++;
}
else
{
result.Append(value[i]);
}
isFirstLine = false;
segmentStart = i + 1;
}
var lastSegment = value[segmentStart..];
result.Append(isFirstLine ? lastSegment : StripDangerousLeadingChars(lastSegment));
return result.ToString();

Copilot uses AI. Check for mistakes.
@Chris0Jeky Chris0Jeky deleted the fix/adversarial-review-787-round2 branch April 9, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants