Skip to content

Commit 2f3bdd4

Browse files
authored
Fix Force Enclosure and Escape character combinaison and Header (#588)
When Writer::forceEnclosure() runs with an empty escape character, the two step str_replace map built in resetProperties() cancels itself: the first pass doubles every enclosure and the second pass (which degenerates to the same "" search) collapses it back. Embedded enclosures are therefore never doubled and the writer emits CSV its own Reader parses incorrectly. Build the replacement map as plain RFC-4180 enclosure doubling when the escape is empty, matching the fputcsv based necessaryEnclosure path. The non-empty escape branch is unchanged.
1 parent 925b07a commit 2f3bdd4

4 files changed

Lines changed: 26 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ All Notable changes to `Csv` will be documented in this file
1919
- `Writer::forceEnclosure` now doubles embedded enclosures when the escape character is the empty string.
2020
- `EscapeFormula` round-trip now preserves fields starting with the escape character
2121
- BOM handling is re-written to fix [#586]([https://github](https://github.com/thephpleague/csv/issues/586))
22+
- CSV Header during CSV download.
2223

2324
### Remove
2425

src/HttpHeaders.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ public static function forFileDownload(string $filename, string $contentType): v
5050
$disposition = 'attachment;filename="'.str_replace('"', '\\"', $fallbackName).'"';
5151
if ($filename !== $fallbackName) {
5252
$disposition .= ";filename*=UTF-8''".preg_replace_callback(
53-
'/[%"\x00-\x1F\x7F-\xFF]/',
54-
static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
55-
$filename
53+
pattern:'/[^A-Za-z0-9!#$&+\-.^_`|~]/',
54+
callback: static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
55+
subject: $filename
5656
);
5757
}
5858

src/Writer.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ protected function insertRecord(array $record): int|false
7575
self::ENCLOSE_ALL => $this->document->fwrite(implode(
7676
$this->delimiter,
7777
array_map(
78-
fn ($content) => $this->enclosure.$content.$this->enclosure,
79-
str_replace($this->enclosure_replace[0], $this->enclosure_replace[1], $record)
78+
fn ($content): string =>
79+
$this->enclosure
80+
.str_replace($this->enclosure, $this->enclosure.$this->enclosure, (string) $content)
81+
.$this->enclosure,
82+
$record
8083
)
8184
).$this->newline),
8285
self::ENCLOSE_NONE => $this->document->fwrite(implode($this->delimiter, $record).$this->newline),

src/WriterTest.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public function testForceEnclosure(): void
189189

190190
self::assertStringContainsString('"1","2"'."\n", $csv);
191191
self::assertStringContainsString('"value 2-0","value 2-1"'."\n", $csv);
192-
self::assertStringContainsString('"to""to","foo\"bar"'."\n", $csv);
192+
self::assertStringContainsString('"to""to","foo\""bar"'."\n", $csv);
193193

194194
$writer->necessaryEnclosure();
195195
self::assertFalse($writer->encloseAll());
@@ -258,7 +258,22 @@ public function testForceEnclosureWithLegacyEscapeIsUnchanged(): void
258258
$writer->insertOne(['to"to', 'foo\"bar']);
259259

260260
// the legacy non-empty escape path must keep doubling once, never twice
261-
self::assertSame('"to""to","foo\"bar"'."\n", $writer->toString());
261+
self::assertSame('"to""to","foo\""bar"'."\n", $writer->toString());
262+
}
263+
264+
public function test_security_fix_force_enclosure(): void
265+
{
266+
$record = ['a"b,INJECTED', 'col2'];
267+
$writer = Writer::fromString()
268+
->forceEnclosure();
269+
270+
$writer->insertOne($record);
271+
self::assertSame('"a""b,INJECTED","col2"'."\n", $writer->toString());
272+
273+
$reader = Reader::fromString($writer->toString())
274+
->setEscape('');
275+
276+
self::assertSame($record, $reader->first());
262277
}
263278

264279
public function testAddValidationRules(): void

0 commit comments

Comments
 (0)