@@ -20,7 +20,8 @@ export function exportAsCSV(report: ReportSchema, data: any[], config?: ReportEx
2020 const fields = report . fields || [ ] ;
2121 const headers = fields . map ( f => f . label || f . name ) ;
2222 const rows = data . map ( row => fields . map ( f => {
23- const val = row [ f . name ] ;
23+ let val = row [ f . name ] ;
24+ val = sanitizeCSVValue ( val ) ;
2425 // Escape CSV values
2526 if ( typeof val === 'string' && ( val . includes ( ',' ) || val . includes ( '"' ) || val . includes ( '\n' ) ) ) {
2627 return `"${ val . replace ( / " / g, '""' ) } "` ;
@@ -55,15 +56,16 @@ export function exportAsJSON(report: ReportSchema, data: any[], config?: ReportE
5556 */
5657export function exportAsHTML ( report : ReportSchema , data : any [ ] , config ?: ReportExportConfig ) : void {
5758 const fields = report . fields || [ ] ;
58- const orientation = config ?. orientation || 'portrait' ;
59+ const orientation = validateOrientation ( config ?. orientation ) ;
60+ const pageSize = validatePageSize ( config ?. pageSize ) ;
5961
6062 const html = `<!DOCTYPE html>
6163<html>
6264<head>
6365<meta charset="utf-8">
6466<title>${ escapeHTML ( report . title || 'Report' ) } </title>
6567<style>
66- @page { size: ${ config ?. pageSize || 'A4' } ${ orientation } ; margin: 20mm; }
68+ @page { size: ${ pageSize } ${ orientation } ; margin: 20mm; }
6769 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; }
6870 h1 { font-size: 24px; margin-bottom: 8px; }
6971 .description { color: #666; margin-bottom: 24px; }
@@ -93,7 +95,8 @@ ${report.description ? `<p class="description">${escapeHTML(report.description)}
9395 */
9496export function exportAsPDF ( report : ReportSchema , data : any [ ] , config ?: ReportExportConfig ) : void {
9597 const fields = report . fields || [ ] ;
96- const orientation = config ?. orientation || 'portrait' ;
98+ const orientation = validateOrientation ( config ?. orientation ) ;
99+ const pageSize = validatePageSize ( config ?. pageSize ) ;
97100
98101 const printWindow = window . open ( '' , '_blank' ) ;
99102 if ( ! printWindow ) {
@@ -113,7 +116,7 @@ export function exportAsPDF(report: ReportSchema, data: any[], config?: ReportEx
113116<meta charset="utf-8">
114117<title>${ escapeHTML ( report . title || 'Report' ) } </title>
115118<style>
116- @page { size: ${ config ?. pageSize || 'A4' } ${ orientation } ; margin: 20mm; }
119+ @page { size: ${ pageSize } ${ orientation } ; margin: 20mm; }
117120 @media print { .no-print { display: none; } }
118121 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; }
119122 h1 { font-size: 24px; margin-bottom: 8px; }
@@ -149,7 +152,8 @@ export function exportAsExcel(report: ReportSchema, data: any[], config?: Report
149152 const fields = report . fields || [ ] ;
150153 const headers = fields . map ( f => f . label || f . name ) ;
151154 const rows = data . map ( row => fields . map ( f => {
152- const val = row [ f . name ] ;
155+ let val = row [ f . name ] ;
156+ val = sanitizeCSVValue ( val ) ;
153157 if ( typeof val === 'string' && ( val . includes ( ',' ) || val . includes ( '"' ) || val . includes ( '\n' ) || val . includes ( '\t' ) ) ) {
154158 return `"${ val . replace ( / " / g, '""' ) } "` ;
155159 }
@@ -207,6 +211,35 @@ function escapeHTML(str: string): string {
207211 . replace ( / ' / g, ''' ) ;
208212}
209213
214+ /**
215+ * Sanitize CSV/Excel cell values to prevent formula injection.
216+ * Prefixes values starting with formula characters (=, +, -, @, |) with a tab.
217+ */
218+ function sanitizeCSVValue ( val : any ) : any {
219+ if ( typeof val === 'string' && val . length > 0 ) {
220+ const firstChar = val . charAt ( 0 ) ;
221+ if ( firstChar === '=' || firstChar === '+' || firstChar === '-' || firstChar === '@' || firstChar === '|' ) {
222+ return `\t${ val } ` ;
223+ }
224+ }
225+ return val ;
226+ }
227+
228+ /**
229+ * Validate page size against allowed values
230+ */
231+ function validatePageSize ( pageSize : string | undefined ) : string {
232+ const allowed = [ 'A4' , 'A3' , 'Letter' , 'Legal' ] ;
233+ return allowed . includes ( pageSize || '' ) ? pageSize ! : 'A4' ;
234+ }
235+
236+ /**
237+ * Validate orientation against allowed values
238+ */
239+ function validateOrientation ( orientation : string | undefined ) : string {
240+ return orientation === 'landscape' ? 'landscape' : 'portrait' ;
241+ }
242+
210243/**
211244 * Helper to trigger file download
212245 */
0 commit comments