svn: /pear/pear-core/tags/PEAR-1.9.3/PEAR/ REST.php

[email protected] (Helgi Þormar Þorbjörnsson) Tue, 08 Mar 2011 23:16:30 +0000
Newsgroups php.pear.cvs,php.pear.core
Message-ID <[email protected]>
dufuz                                    Tue, 08 Mar 2011 23:16:30 +0000

Revision: http://svn.php.net/viewvc?view=revision&revision=309042

Log:
Introduce a time-of-check-time-of-use (TOCTOU) save file handler, this makes sure that from the time we check it is available until we get the file handler that no one attempted to put in a soft / hard link instead of what we checked for.

Further improves on #18056

Bug: http://pear.php.net/bugs/18056 (unknown) 
      
Changed paths:
    U   pear/pear-core/tags/PEAR-1.9.3/PEAR/REST.php

Modified: pear/pear-core/tags/PEAR-1.9.3/PEAR/REST.php
===================================================================
--- pear/pear-core/tags/PEAR-1.9.3/PEAR/REST.php	2011-03-08 22:46:27 UTC (rev 309041)
+++ pear/pear-core/tags/PEAR-1.9.3/PEAR/REST.php	2011-03-08 23:16:30 UTC (rev 309042)
@@ -228,59 +228,75 @@
         $cacheidfile = $d . 'rest.cacheid';
         $cachefile   = $d . 'rest.cachefile';

+        if (!is_dir($cache_dir)) {
+            if (System::mkdir(array('-p', $cache_dir) === false)) {
+              return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory and attempts to create the directory failed.");
+            }
+        }
+
         if ($cacheid === null && $nochange) {
             $cacheid = unserialize(implode('', file($cacheidfile)));
         }

-        if (is_link($cacheidfile)) {
-            return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $cacheidfile . ' as it is symlinked to ' . readlink($cacheidfile) . ' - Possible symlink attack');
-        }
+        $idData = serialize(array(
+            'age'        => time(),
+            'lastChange' => ($nochange ? $cacheid['lastChange'] : $lastmodified),
+        ));

-        if (is_link($cachefile)) {
-            return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $cacheidfile . ' as it is symlinked to ' . readlink($cacheidfile) . ' - Possible symlink attack');
+        $result = $this->saveCacheFile($cacheidfile, $idData);
+        if (PEAR::isError($result)) {
+            return $result;
+        } elseif ($nochange) {
+            return true;
         }

-        $cacheidfile_fp = @fopen($cacheidfile, 'wb');
-        if (!$cacheidfile_fp) {
-            if (is_dir($cache_dir)) {
-                return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory. ");
+        $result = $this->saveCacheFile($cachefile, serialize($contents));
+        if (PEAR::isError($result)) {
+            if (file_exists($cacheidfile)) {
+              @unlink($cacheidfile);
             }

-            System::mkdir(array('-p', $cache_dir));
-            $cacheidfile_fp = @fopen($cacheidfile, 'wb');
-            if (!$cacheidfile_fp) {
-                return PEAR::raiseError("Could not open $cacheidfile for writing.");
-            }
+            return $result;
         }

-        if ($nochange) {
-            fwrite($cacheidfile_fp, serialize(array(
-                'age'        => time(),
-                'lastChange' => $cacheid['lastChange'],
-                ))
-            );
+        return true;
+    }

-            fclose($cacheidfile_fp);
-            return true;
-        }
+    function saveCacheFile($file, $contents)
+    {
+        $len = strlen($contents);

-        fwrite($cacheidfile_fp, serialize(array(
-            'age'        => time(),
-            'lastChange' => $lastmodified,
-            ))
-        );
-        fclose($cacheidfile_fp);
+        $cachefile_fp = @fopen($file, 'xb'); // x is the O_CREAT|O_EXCL mode
+        if ($cachefile_fp !== false) { // create file
+            if (fwrite($cachefile_fp, $contents, $len) < $len) {
+                fclose($cachefile_fp);
+                return PEAR::raiseError("Could not write $file.");
+            }
+        } else { // update file
+            $cachefile_lstat = lstat($file);
+            $cachefile_fp = @fopen($file, 'wb');
+            if (!$cachefile_fp) {
+                return PEAR::raiseError("Could not open $file for writing.");
+            }

-        $cachefile_fp = @fopen($cachefile, 'wb');
-        if (!$cachefile_fp) {
-            if (file_exists($cacheidfile)) {
-                @unlink($cacheidfile);
+            $cachefile_fstat = fstat($cachefile_fp);
+            if (
+              $cachefile_lstat['mode'] == $cachefile_fstat['mode'] &&
+              $cachefile_lstat['ino']  == $cachefile_fstat['ino'] &&
+              $cachefile_lstat['dev']  == $cachefile_fstat['dev'] &&
+              $cachefile_fstat['nlink'] === 1
+            ) {
+                if (fwrite($cachefile_fp, $contents, $len) < $len) {
+                    fclose($cachefile_fp);
+                    return PEAR::raiseError("Could not write $file.");
+                }
+            } else {
+                fclose($cachefile_fp);
+                $link = function_exists('readlink') ? readlink($file) : $file;
+                return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $file . ' as it is symlinked to ' . $link . ' - Possible symlink attack');
             }
-
-            return PEAR::raiseError("Could not open $cacheidfile for writing.");
         }

-        fwrite($cachefile_fp, serialize($contents));
         fclose($cachefile_fp);
         return true;
     }
@@ -464,4 +480,4 @@

         return $data;
     }
-}
+}
\ No newline at end of file