Skip to content

Commit 99ed66b

Browse files
committed
Fix GH-20582: Heap Buffer Overflow in iptcembed
If you can extend the file between the file size gathering (resulting in a buffer allocation), and reading / writing to the file you can trigger a TOC-TOU where you write out of bounds. To solve this, add extra bound checks and make sure that write actions always fail when going out of bounds. The easiest way to trigger this is via a pipe, which is used in the test, but it should be possible with a regular file and a quick race condition as well. Closes GH-20591.
1 parent 494dd97 commit 99ed66b

File tree

3 files changed

+105
-28
lines changed

3 files changed

+105
-28
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ PHP NEWS
5252

5353
- Standard:
5454
. Fix error check for proc_open() command. (ndossche)
55+
. Fixed bug GH-20582 (Heap Buffer Overflow in iptcembed). (ndossche)
5556

5657
18 Dec 2025, PHP 8.3.29
5758

ext/standard/iptc.c

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,24 @@
7373
#define M_APP15 0xef
7474

7575
/* {{{ php_iptc_put1 */
76-
static int php_iptc_put1(FILE *fp, int spool, unsigned char c, unsigned char **spoolbuf)
76+
static int php_iptc_put1(FILE *fp, int spool, unsigned char c, unsigned char **spoolbuf, const unsigned char *spoolbuf_end)
7777
{
7878
if (spool > 0)
7979
PUTC(c);
8080

81-
if (spoolbuf) *(*spoolbuf)++ = c;
81+
if (spoolbuf) {
82+
if (UNEXPECTED(*spoolbuf >= spoolbuf_end)) {
83+
return EOF;
84+
}
85+
*(*spoolbuf)++ = c;
86+
}
8287

8388
return c;
8489
}
8590
/* }}} */
8691

8792
/* {{{ php_iptc_get1 */
88-
static int php_iptc_get1(FILE *fp, int spool, unsigned char **spoolbuf)
93+
static int php_iptc_get1(FILE *fp, int spool, unsigned char **spoolbuf, const unsigned char *spoolbuf_end)
8994
{
9095
int c;
9196
char cc;
@@ -99,66 +104,71 @@ static int php_iptc_get1(FILE *fp, int spool, unsigned char **spoolbuf)
99104
PUTC(cc);
100105
}
101106

102-
if (spoolbuf) *(*spoolbuf)++ = c;
107+
if (spoolbuf) {
108+
if (UNEXPECTED(*spoolbuf >= spoolbuf_end)) {
109+
return EOF;
110+
}
111+
*(*spoolbuf)++ = c;
112+
}
103113

104114
return c;
105115
}
106116
/* }}} */
107117

108118
/* {{{ php_iptc_read_remaining */
109-
static int php_iptc_read_remaining(FILE *fp, int spool, unsigned char **spoolbuf)
119+
static int php_iptc_read_remaining(FILE *fp, int spool, unsigned char **spoolbuf, const unsigned char *spoolbuf_end)
110120
{
111-
while (php_iptc_get1(fp, spool, spoolbuf) != EOF) continue;
121+
while (php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end) != EOF) continue;
112122

113123
return M_EOI;
114124
}
115125
/* }}} */
116126

117127
/* {{{ php_iptc_skip_variable */
118-
static int php_iptc_skip_variable(FILE *fp, int spool, unsigned char **spoolbuf)
128+
static int php_iptc_skip_variable(FILE *fp, int spool, unsigned char **spoolbuf, const unsigned char *spoolbuf_end)
119129
{
120130
unsigned int length;
121131
int c1, c2;
122132

123-
if ((c1 = php_iptc_get1(fp, spool, spoolbuf)) == EOF) return M_EOI;
133+
if ((c1 = php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end)) == EOF) return M_EOI;
124134

125-
if ((c2 = php_iptc_get1(fp, spool, spoolbuf)) == EOF) return M_EOI;
135+
if ((c2 = php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end)) == EOF) return M_EOI;
126136

127137
length = (((unsigned char) c1) << 8) + ((unsigned char) c2);
128138

129139
length -= 2;
130140

131141
while (length--)
132-
if (php_iptc_get1(fp, spool, spoolbuf) == EOF) return M_EOI;
142+
if (php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end) == EOF) return M_EOI;
133143

134144
return 0;
135145
}
136146
/* }}} */
137147

138148
/* {{{ php_iptc_next_marker */
139-
static int php_iptc_next_marker(FILE *fp, int spool, unsigned char **spoolbuf)
149+
static int php_iptc_next_marker(FILE *fp, int spool, unsigned char **spoolbuf, const unsigned char *spoolbuf_end)
140150
{
141151
int c;
142152

143153
/* skip unimportant stuff */
144154

145-
c = php_iptc_get1(fp, spool, spoolbuf);
155+
c = php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end);
146156

147157
if (c == EOF) return M_EOI;
148158

149159
while (c != 0xff) {
150-
if ((c = php_iptc_get1(fp, spool, spoolbuf)) == EOF)
160+
if ((c = php_iptc_get1(fp, spool, spoolbuf, spoolbuf_end)) == EOF)
151161
return M_EOI; /* we hit EOF */
152162
}
153163

154164
/* get marker byte, swallowing possible padding */
155165
do {
156-
c = php_iptc_get1(fp, 0, 0);
166+
c = php_iptc_get1(fp, 0, 0, NULL);
157167
if (c == EOF)
158168
return M_EOI; /* we hit EOF */
159169
else
160170
if (c == 0xff)
161-
php_iptc_put1(fp, spool, (unsigned char)c, spoolbuf);
171+
php_iptc_put1(fp, spool, (unsigned char)c, spoolbuf, spoolbuf_end);
162172
} while (c == 0xff);
163173

164174
return (unsigned int) c;
@@ -178,6 +188,7 @@ PHP_FUNCTION(iptcembed)
178188
size_t inx;
179189
zend_string *spoolbuf = NULL;
180190
unsigned char *poi = NULL;
191+
unsigned char *spoolbuf_end = NULL;
181192
zend_stat_t sb = {0};
182193
bool written = 0;
183194

@@ -210,18 +221,20 @@ PHP_FUNCTION(iptcembed)
210221

211222
spoolbuf = zend_string_safe_alloc(1, iptcdata_len + sizeof(psheader) + 1024 + 1, sb.st_size, 0);
212223
poi = (unsigned char*)ZSTR_VAL(spoolbuf);
224+
spoolbuf_end = poi + ZSTR_LEN(spoolbuf);
213225
memset(poi, 0, iptcdata_len + sizeof(psheader) + sb.st_size + 1024 + 1);
214226
}
215227

216-
if (php_iptc_get1(fp, spool, poi?&poi:0) != 0xFF) {
228+
if (php_iptc_get1(fp, spool, poi?&poi:0, spoolbuf_end) != 0xFF) {
217229
fclose(fp);
218230
if (spoolbuf) {
219231
zend_string_efree(spoolbuf);
220232
}
221233
RETURN_FALSE;
222234
}
223235

224-
if (php_iptc_get1(fp, spool, poi?&poi:0) != 0xD8) {
236+
if (php_iptc_get1(fp, spool, poi?&poi:0, spoolbuf_end) != 0xD8) {
237+
err:
225238
fclose(fp);
226239
if (spoolbuf) {
227240
zend_string_efree(spoolbuf);
@@ -230,20 +243,22 @@ PHP_FUNCTION(iptcembed)
230243
}
231244

232245
while (!done) {
233-
marker = php_iptc_next_marker(fp, spool, poi?&poi:0);
246+
marker = php_iptc_next_marker(fp, spool, poi?&poi:0, spoolbuf_end);
234247

235248
if (marker == M_EOI) { /* EOF */
236249
break;
237250
} else if (marker != M_APP13) {
238-
php_iptc_put1(fp, spool, (unsigned char)marker, poi?&poi:0);
251+
if (php_iptc_put1(fp, spool, (unsigned char)marker, poi?&poi:0, spoolbuf_end) < 0) {
252+
goto err;
253+
}
239254
}
240255

241256
switch (marker) {
242257
case M_APP13:
243258
/* we are going to write a new APP13 marker, so don't output the old one */
244-
php_iptc_skip_variable(fp, 0, 0);
259+
php_iptc_skip_variable(fp, 0, 0, spoolbuf_end);
245260
fgetc(fp); /* skip already copied 0xFF byte */
246-
php_iptc_read_remaining(fp, spool, poi?&poi:0);
261+
php_iptc_read_remaining(fp, spool, poi?&poi:0, spoolbuf_end);
247262
done = 1;
248263
break;
249264

@@ -256,7 +271,7 @@ PHP_FUNCTION(iptcembed)
256271
}
257272
written = 1;
258273

259-
php_iptc_skip_variable(fp, spool, poi?&poi:0);
274+
php_iptc_skip_variable(fp, spool, poi?&poi:0, spoolbuf_end);
260275

261276
if (iptcdata_len & 1) {
262277
iptcdata_len++; /* make the length even */
@@ -266,32 +281,41 @@ PHP_FUNCTION(iptcembed)
266281
psheader[ 3 ] = (iptcdata_len+28)&0xff;
267282

268283
for (inx = 0; inx < 28; inx++) {
269-
php_iptc_put1(fp, spool, psheader[inx], poi?&poi:0);
284+
if (php_iptc_put1(fp, spool, psheader[inx], poi?&poi:0, spoolbuf_end) < 0) {
285+
goto err;
286+
}
270287
}
271288

272-
php_iptc_put1(fp, spool, (unsigned char)(iptcdata_len>>8), poi?&poi:0);
273-
php_iptc_put1(fp, spool, (unsigned char)(iptcdata_len&0xff), poi?&poi:0);
289+
if (php_iptc_put1(fp, spool, (unsigned char)(iptcdata_len>>8), poi?&poi:0, spoolbuf_end) < 0) {
290+
goto err;
291+
}
292+
if (php_iptc_put1(fp, spool, (unsigned char)(iptcdata_len&0xff), poi?&poi:0, spoolbuf_end) < 0) {
293+
goto err;
294+
}
274295

275296
for (inx = 0; inx < iptcdata_len; inx++) {
276-
php_iptc_put1(fp, spool, iptcdata[inx], poi?&poi:0);
297+
if (php_iptc_put1(fp, spool, iptcdata[inx], poi?&poi:0, spoolbuf_end) < 0) {
298+
goto err;
299+
}
277300
}
278301
break;
279302

280303
case M_SOS:
281304
/* we hit data, no more marker-inserting can be done! */
282-
php_iptc_read_remaining(fp, spool, poi?&poi:0);
305+
php_iptc_read_remaining(fp, spool, poi?&poi:0, spoolbuf_end);
283306
done = 1;
284307
break;
285308

286309
default:
287-
php_iptc_skip_variable(fp, spool, poi?&poi:0);
310+
php_iptc_skip_variable(fp, spool, poi?&poi:0, spoolbuf_end);
288311
break;
289312
}
290313
}
291314

292315
fclose(fp);
293316

294317
if (spool < 2) {
318+
*poi = '\0';
295319
spoolbuf = zend_string_truncate(spoolbuf, poi - (unsigned char*)ZSTR_VAL(spoolbuf), 0);
296320
RETURN_NEW_STR(spoolbuf);
297321
} else {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
GH-20582 (Heap Buffer Overflow in iptcembed)
3+
--CREDITS--
4+
Nikita Sveshnikov (Positive Technologies)
5+
ndossche
6+
--SKIPIF--
7+
<?php
8+
if (PHP_OS_FAMILY === "Windows") die("skip Only for platforms with FIFO pipes");
9+
?>
10+
--FILE--
11+
<?php
12+
13+
$pipe = __DIR__.'/gh20582.pipe.jpg';
14+
15+
// Create named pipe (FIFO)
16+
if (!file_exists($pipe)) {
17+
if (!posix_mkfifo($pipe, 0666)) {
18+
die("Failed to create FIFO\n");
19+
}
20+
}
21+
22+
$descriptorspec = array(
23+
0 => STDIN,
24+
1 => STDOUT,
25+
2 => STDOUT,
26+
);
27+
$pipes = [];
28+
$proc = proc_open([PHP_BINARY, '-n', '-r', "var_dump(iptcembed('A', '$pipe'));"], $descriptorspec, $pipes);
29+
30+
// Blocks until a reader opens it
31+
$fp = fopen($pipe, 'wb') or die("Failed to open FIFO");
32+
33+
// Write header
34+
$data = "\xFF\xD8"; // SOI marker
35+
$data .= "\xFF\xE0\x00\x10"; // APP0 marker (JFIF)
36+
$data .= "JFIF" . str_repeat("\x00", 9);
37+
$data .= "\xFF\xDA\x00\x08"; // SOS marker
38+
$data .= str_repeat("\x00", 6);
39+
fwrite($fp, $data);
40+
41+
// Write garbage
42+
fwrite($fp, str_repeat("A", 5120));
43+
44+
fclose($fp);
45+
46+
?>
47+
--CLEAN--
48+
<?php
49+
@unlink(__DIR__.'/gh20582.pipe.jpg');
50+
?>
51+
--EXPECTF--
52+
string(1055) "ÿØÿà%0JFIF%0%0%0%0%0%0%0%0%0ÿÿí%0Photoshop 3.0%08BIM%0%0%0%0%0A%0Ú%0%0%0%0%0%0%0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

0 commit comments

Comments
 (0)