Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/OSFramework/DataGrid/Feature/ExposedFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace OSFramework.DataGrid.Feature {
public autoRowNumber: IRowNumber;
public calculatedField: ICalculatedField;
public cellData: ICellData;
public cellDataSanitizer: ICellDataSanitizer;
public cellStyle: ICellStyle;
public clickEvent: IClickEvent;
public column: IColumn;
Expand Down
6 changes: 6 additions & 0 deletions src/OSFramework/DataGrid/Feature/ICellDataSanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.DataGrid.Feature {
export interface ICellDataSanitizer {
escapeCsvInjection(cellString: string): string | null;
}
}
78 changes: 78 additions & 0 deletions src/Providers/DataGrid/Wijmo/Features/CellDataSanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace Providers.DataGrid.Wijmo.Feature {
export class CellDataSanitizer
implements OSFramework.DataGrid.Feature.ICellDataSanitizer, OSFramework.DataGrid.Interface.IBuilder
{
// Characters that can trigger CSV injection by being interpreted as formula starts in spreadsheet applications (Excel, LibreOffice, etc.)
private readonly _dangerousStarts = ['=', '+', '-', '@'];
private readonly _grid: Grid.IGridWijmo;

constructor(grid: Grid.IGridWijmo) {
this._grid = grid;
}

public build(): void {
// Callback for when the grid is being exported to CSV.
// Made available in the Wijmo 2025 v2 (Build 5.20252.42).
this._grid.provider.gettingCellClipString.addHandler(
(s: wijmo.grid.FlexGrid, e: wijmo.grid.CellRangeEventArgs) => {
e.data = this.escapeCsvInjection(e.data);
}
);
}

/**
* Mitigates CSV/Excel formula injection by neutralizing values that could be
* interpreted as formulas by spreadsheet applications.
*
* A value is considered dangerous if it starts with any of the characters
* defined in `_dangerousStarts` (`=`, `+`, `-`, `@`), either directly, inside
* an initial double-quoted field (e.g. `"=1+1"`), or immediately after a
* tab, newline, or carriage-return character within the string.
*
* Escaping strategy:
* - If the string starts with a dangerous character (e.g. `=1+1`), a single
* quote (`'`) is prepended (resulting in `'=1+1`), so the value is treated
* as literal text by most CSV/Excel consumers.
* - If the string starts with a double quote followed by a dangerous
* character (e.g. `"=1+1"`), a single quote is inserted after the opening
* quote (resulting in `"'=1+1"`).
* - If a dangerous character appears immediately after a tab, newline, or
* carriage-return character, a single quote is inserted between the
* whitespace and the dangerous character (e.g. `\n=1+1` becomes
* `\n'=1+1`).
*
* This function does not perform general CSV quoting/escaping; it only
* addresses formula-like patterns to reduce the risk of CSV injection.
*
* @param cellString Raw cell content to be exported to CSV. If this value is
* falsy (e.g. empty string), it is returned as-is without modification.
* @returns The sanitized string with potentially dangerous formula prefixes
* neutralized, or the original falsy value (such as `''` or `null`)
* unchanged.
*/
public escapeCsvInjection(cellString: string): string | null {
if (!cellString) return cellString;

// Prefix values that start with dangerous characters with a single quote to prevent CSV injection
const needEscape = this._dangerousStarts.some((char) => cellString.startsWith(char));
if (needEscape) {
cellString = `'${cellString}`;
}

// Also handle values that start with a quote followed by a dangerous character
const containsQuote = this._dangerousStarts.some((char) => cellString.startsWith('"' + char));
if (containsQuote) {
cellString = cellString.replace(/^"([=+\-@])/, (match, $1) => '"\'' + $1);
}

// Also handle values that start with a quote followed by a dangerous character
const containsWrap = /[\t\n\r]/.test(cellString);
if (containsWrap) {
cellString = cellString.replace(/([\t\n\r])([=+\-@])/g, (match, $1, $2) => $1 + "'" + $2);
}

return cellString;
}
}
}
8 changes: 4 additions & 4 deletions src/Providers/DataGrid/Wijmo/Features/Export.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace Providers.DataGrid.Wijmo.Feature {
export class Export implements OSFramework.DataGrid.Feature.IExport, OSFramework.DataGrid.Interface.IBuilder {
private _curPage: number;
private _grid: Grid.IGridWijmo;
private _currPage: number;
private readonly _grid: Grid.IGridWijmo;
private _hasLoadingMessage = true;
private _loadingMessage = 'Your data is being exported.';
private _pageSize: number;
Expand All @@ -28,7 +28,7 @@ namespace Providers.DataGrid.Wijmo.Feature {
//Then re-apply the pagination
private _reApplyPagination(): void {
this._grid.features.pagination.changePageSize(this._pageSize);
this._grid.features.pagination.moveToPage(this._curPage);
this._grid.features.pagination.moveToPage(this._currPage);
}

private _removeLoadingMessage() {
Expand All @@ -41,7 +41,7 @@ namespace Providers.DataGrid.Wijmo.Feature {
//Exporting to Excel Consider only the current page, so we need to remove the pagination first of all
private _resetPagination(): void {
this._pageSize = this._grid.features.pagination.pageSize;
this._curPage = this._grid.features.pagination.pageIndex;
this._currPage = this._grid.features.pagination.pageIndex;
this._grid.features.pagination.moveToFirstPage();
this._grid.features.pagination.changePageSize(0);
}
Expand Down
6 changes: 6 additions & 0 deletions src/Providers/DataGrid/Wijmo/Features/FeatureBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ namespace Providers.DataGrid.Wijmo.Feature {
return this;
}

private _makeCellDataSanitizer(): FeatureBuilder {
this._features.cellDataSanitizer = this._makeItem(CellDataSanitizer);
return this;
}

private _makeCellStyle(): FeatureBuilder {
this._features.cellStyle = this._makeItem(CellStyle);
return this;
Expand Down Expand Up @@ -212,6 +217,7 @@ namespace Providers.DataGrid.Wijmo.Feature {
._makeExport()
._makeGroupPanel(config.groupPanelId)
._makeCellData()
._makeCellDataSanitizer()
._makeCellStyle()
._makeTooltip()
._makePagination(config.rowsPerPage)
Expand Down