diff --git a/installer/installer.php b/installer/installer.php index 50657d80..fedb4251 100644 --- a/installer/installer.php +++ b/installer/installer.php @@ -110,6 +110,7 @@ class installer { static function mysql_version($config) { $result = mysql_query("SHOW VARIABLES WHERE variable_name = \"version\""); + $row = mysql_fetch_object($result); return $row->Value; } diff --git a/modules/akismet/helpers/akismet.php b/modules/akismet/helpers/akismet.php index db45a6ab..7cd598cd 100644 --- a/modules/akismet/helpers/akismet.php +++ b/modules/akismet/helpers/akismet.php @@ -94,7 +94,7 @@ class akismet_Core { if (empty($api_key)) { site_status::warning( t("Akismet is not quite ready! Please provide an API Key", - array("url" => url::site("admin/akismet"))), + array("url" => html::mark_safe(url::site("admin/akismet")))), "akismet_config"); } else { site_status::clear("akismet_config"); diff --git a/modules/comment/controllers/comments.php b/modules/comment/controllers/comments.php index 9fb4796e..82b12893 100644 --- a/modules/comment/controllers/comments.php +++ b/modules/comment/controllers/comments.php @@ -39,9 +39,9 @@ class Comments_Controller extends REST_Controller { foreach ($comments as $comment) { $data[] = array( "id" => $comment->id, - "author_name" => p::clean($comment->author_name()), + "author_name" => html::clean($comment->author_name()), "created" => $comment->created, - "text" => nl2br(p::purify($comment->text))); + "text" => nl2br(html::purify($comment->text))); } print json_encode($data); break; @@ -126,9 +126,9 @@ class Comments_Controller extends REST_Controller { array("result" => "success", "data" => array( "id" => $comment->id, - "author_name" => p::clean($comment->author_name()), + "author_name" => html::clean($comment->author_name()), "created" => $comment->created, - "text" => nl2br(p::purify($comment->text))))); + "text" => nl2br(html::purify($comment->text))))); } else { $view = new Theme_View("comment.html", "fragment"); $view->comment = $comment; diff --git a/modules/comment/helpers/comment_rss.php b/modules/comment/helpers/comment_rss.php index e233de59..b539887b 100644 --- a/modules/comment/helpers/comment_rss.php +++ b/modules/comment/helpers/comment_rss.php @@ -23,7 +23,7 @@ class comment_rss_Core { $feeds["comment/newest"] = t("All new comments"); if ($item) { $feeds["comment/item/$item->id"] = - t("Comments on %title", array("title" => p::purify($item->title))); + t("Comments on %title", array("title" => html::purify($item->title))); } return $feeds; } @@ -49,13 +49,13 @@ class comment_rss_Core { $item = $comment->item(); $feed->children[] = new ArrayObject( array("pub_date" => date("D, d M Y H:i:s T", $comment->created), - "text" => nl2br(p::purify($comment->text)), + "text" => nl2br(html::purify($comment->text)), "thumb_url" => $item->thumb_url(), "thumb_height" => $item->thumb_height, "thumb_width" => $item->thumb_width, "item_uri" => url::abs_site("{$item->type}s/$item->id"), - "title" => p::purify($item->title), - "author" => p::clean($comment->author_name())), + "title" => html::purify($item->title), + "author" => html::clean($comment->author_name())), ArrayObject::ARRAY_AS_PROPS); } diff --git a/modules/comment/views/admin_block_recent_comments.html.php b/modules/comment/views/admin_block_recent_comments.html.php index 516a8181..dc3975e0 100644 --- a/modules/comment/views/admin_block_recent_comments.html.php +++ b/modules/comment/views/admin_block_recent_comments.html.php @@ -4,13 +4,13 @@
  • "> " class="gAvatar" - alt="author_name()) ?>" + alt="author_name()) ?>" width="32" height="32" /> created) ?> %author_name said %comment_text', - array("author_name" => p::clean($comment->author_name()), - "comment_text" => text::limit_words(nl2br(p::purify($comment->text)), 50))); ?> + array("author_name" => html::clean($comment->author_name()), + "comment_text" => text::limit_words(nl2br(html::purify($comment->text)), 50))); ?>
  • diff --git a/modules/comment/views/admin_comments.html.php b/modules/comment/views/admin_comments.html.php index 03511d91..801ce2b3 100644 --- a/modules/comment/views/admin_comments.html.php +++ b/modules/comment/views/admin_comments.html.php @@ -108,12 +108,12 @@ " class="gAvatar" - alt="author_name()) ?>" + alt="author_name()) ?>" width="40" height="40" /> -

    author_name()) ?>

    +

    author_name()) ?>

    @@ -122,7 +122,7 @@ has_thumb()): ?> <?= p::purify($item->title) ?>thumb_width, $item->thumb_height, 75) ?> /> @@ -132,7 +132,7 @@

    created) ?>

    - text)) ?> + text)) ?>
    diff --git a/modules/gallery/controllers/admin_advanced_settings.php b/modules/gallery/controllers/admin_advanced_settings.php index 64007fdb..43c77340 100644 --- a/modules/gallery/controllers/admin_advanced_settings.php +++ b/modules/gallery/controllers/admin_advanced_settings.php @@ -46,7 +46,7 @@ class Admin_Advanced_Settings_Controller extends Admin_Controller { module::set_var($module_name, $var_name, Input::instance()->post("value")); message::success( t("Saved value for %var (%module_name)", - array("var" => p::clean($var_name), "module_name" => $module_name))); + array("var" => html::clean($var_name), "module_name" => $module_name))); print json_encode(array("result" => "success")); } diff --git a/modules/gallery/controllers/admin_languages.php b/modules/gallery/controllers/admin_languages.php index 6dc242c6..d85c47f9 100644 --- a/modules/gallery/controllers/admin_languages.php +++ b/modules/gallery/controllers/admin_languages.php @@ -21,10 +21,10 @@ class Admin_Languages_Controller extends Admin_Controller { public function index($share_translations_form=null) { $v = new Admin_View("admin.html"); $v->content = new View("admin_languages.html"); - $v->content->available_locales = locales::available(); + $v->content->available_locales = locales::available(); $v->content->installed_locales = locales::installed(); $v->content->default_locale = module::get_var("gallery", "default_locale"); - + if (empty($share_translations_form)) { $share_translations_form = $this->_share_translations_form(); } @@ -35,21 +35,21 @@ class Admin_Languages_Controller extends Admin_Controller { public function save() { access::verify_csrf(); - - locales::update_installed($this->input->post("installed_locales")); - - $installed_locales = array_keys(locales::installed()); + + locales::update_installed($this->input->post("installed_locales")); + + $installed_locales = array_keys(locales::installed()); $new_default_locale = $this->input->post("default_locale"); - if (!in_array($new_default_locale, $installed_locales)) { - if (!empty($installed_locales)) { - $new_default_locale = $installed_locales[0]; - } else { - $new_default_locale = "en_US"; - } - } - module::set_var("gallery", "default_locale", $new_default_locale); - - print json_encode(array("result" => "success")); + if (!in_array($new_default_locale, $installed_locales)) { + if (!empty($installed_locales)) { + $new_default_locale = $installed_locales[0]; + } else { + $new_default_locale = "en_US"; + } + } + module::set_var("gallery", "default_locale", $new_default_locale); + + print json_encode(array("result" => "success")); } public function share() { @@ -111,7 +111,7 @@ class Admin_Languages_Controller extends Admin_Controller { $group->input("api_key") ->label(empty($api_key) ? t("This is a unique key that will allow you to send translations to the remote server. To get your API key go to %server-link.", - array("server-link" => html::anchor($server_link))) + array("server-link" => html::mark_safe(html::anchor($server_link)))) : t("API Key")) ->value($api_key) ->error_messages("invalid", t("The API key you provided is invalid.")); diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php index cdfa823d..ec3eb426 100644 --- a/modules/gallery/controllers/albums.php +++ b/modules/gallery/controllers/albums.php @@ -112,7 +112,7 @@ class Albums_Controller extends Items_Controller { log::success("content", "Created an album", html::anchor("albums/$new_album->id", "view album")); message::success( - t("Created album %album_title", array("album_title" => p::clean($new_album->title)))); + t("Created album %album_title", array("album_title" => $new_album->title))); print json_encode( array("result" => "success", @@ -145,7 +145,7 @@ class Albums_Controller extends Items_Controller { log::success("content", "Added a photo", html::anchor("photos/$photo->id", "view photo")); message::success( - t("Added photo %photo_title", array("photo_title" => p::clean($photo->title)))); + t("Added photo %photo_title", array("photo_title" => $photo->title))); print json_encode( array("result" => "success", @@ -194,7 +194,7 @@ class Albums_Controller extends Items_Controller { log::success("content", "Updated album", "id\">view"); message::success( - t("Saved album %album_title", array("album_title" => p::clean($album->title)))); + t("Saved album %album_title", array("album_title" => $album->title))); print json_encode( array("result" => "success", diff --git a/modules/gallery/controllers/l10n_client.php b/modules/gallery/controllers/l10n_client.php index 0775791e..16d39024 100644 --- a/modules/gallery/controllers/l10n_client.php +++ b/modules/gallery/controllers/l10n_client.php @@ -90,13 +90,13 @@ class L10n_Client_Controller extends Controller { } $session = Session::instance(); - $l10n_mode = $session->get("l10n_mode", false); + $l10n_mode = $session->get("l10n_mode", false); $session->set("l10n_mode", !$l10n_mode); $redirect_url = "admin/languages"; - if (!$l10n_mode) { - $redirect_url .= "#l10n-client"; - } + if (!$l10n_mode) { + $redirect_url .= "#l10n-client"; + } url::redirect($redirect_url); } diff --git a/modules/gallery/controllers/movies.php b/modules/gallery/controllers/movies.php index c8227d74..09b16759 100644 --- a/modules/gallery/controllers/movies.php +++ b/modules/gallery/controllers/movies.php @@ -93,7 +93,7 @@ class Movies_Controller extends Items_Controller { log::success("content", "Updated photo", "id\">view"); message::success( - t("Saved photo %photo_title", array("photo_title" => p::clean($photo->title)))); + t("Saved photo %photo_title", array("photo_title" => $photo->title))); print json_encode( array("result" => "success", diff --git a/modules/gallery/controllers/photos.php b/modules/gallery/controllers/photos.php index 8ee24da8..3447b4c6 100644 --- a/modules/gallery/controllers/photos.php +++ b/modules/gallery/controllers/photos.php @@ -86,7 +86,7 @@ class Photos_Controller extends Items_Controller { log::success("content", "Updated photo", "id\">view"); message::success( - t("Saved photo %photo_title", array("photo_title" => p::clean($photo->title)))); + t("Saved photo %photo_title", array("photo_title" => $photo->title))); print json_encode( array("result" => "success", diff --git a/modules/gallery/controllers/quick.php b/modules/gallery/controllers/quick.php index 82176e02..20731f9c 100644 --- a/modules/gallery/controllers/quick.php +++ b/modules/gallery/controllers/quick.php @@ -75,7 +75,7 @@ class Quick_Controller extends Controller { access::required("view", $item->parent()); access::required("edit", $item->parent()); - $msg = t("Made %title this album's cover", array("title" => p::purify($item->title))); + $msg = t("Made %title this album's cover", array("title" => html::purify($item->title))); item::make_album_cover($item); message::success($msg); @@ -91,10 +91,10 @@ class Quick_Controller extends Controller { if ($item->is_album()) { print t( "Delete the album %title? All photos and movies in the album will also be deleted.", - array("title" => p::purify($item->title))); + array("title" => html::purify($item->title))); } else { print t("Are you sure you want to delete %title?", - array("title" => p::purify($item->title))); + array("title" => html::purify($item->title))); } $form = item::get_delete_form($item); @@ -108,9 +108,9 @@ class Quick_Controller extends Controller { access::required("edit", $item); if ($item->is_album()) { - $msg = t("Deleted album %title", array("title" => p::purify($item->title))); + $msg = t("Deleted album %title", array("title" => html::purify($item->title))); } else { - $msg = t("Deleted photo %title", array("title" => p::purify($item->title))); + $msg = t("Deleted photo %title", array("title" => html::purify($item->title))); } $parent = $item->parent(); diff --git a/modules/gallery/helpers/MY_html.php b/modules/gallery/helpers/MY_html.php new file mode 100644 index 00000000..4522d01c --- /dev/null +++ b/modules/gallery/helpers/MY_html.php @@ -0,0 +1,91 @@ + + *
    + * + */ + static function clean($html) { + return new SafeString($html); + } + + /** + * Returns a string that is safe to be used in HTML (XSS protection), + * purifying (filtering) the given HTML to ensure that the result contains + * only non-malicious HTML. + * + * Example:
    +   *   
    title) ?> + *
    + */ + static function purify($html) { + return SafeString::purify($html); + } + + /** + * Flags the given string as safe to be used in HTML (free of malicious HTML/JS). + * + * Example:
    +   *   // Parameters to t() are automatically escaped by default.
    +   *   // If the parameter is marked as safe, it won't get escaped.
    +   *   t('Go there',
    +   *     array("url" => html::mark_safe(url::current())))
    +   * 
    + */ + static function mark_safe($html) { + return SafeString::of_safe_html($html); + } + + /** + * Escapes the given string for use in JavaScript. + * + * Example:
    +   *   
    +   * 
    + */ + static function js_string($string) { + return SafeString::of($string)->for_js(); + } + + /** + * Returns a string safe for use in HTML element attributes. + * + * Assumes that the HTML element attribute is already + * delimited by single or double quotes + * + * Example:
    +   *     ;
    +   *   
    +   * 
    + * @return the string escaped for use in HTML attributes. + */ + static function clean_attribute($string) { + return self::clean($string)->for_html_attr(); + } +} diff --git a/modules/gallery/helpers/gallery.php b/modules/gallery/helpers/gallery.php index 122227fc..035ed1da 100644 --- a/modules/gallery/helpers/gallery.php +++ b/modules/gallery/helpers/gallery.php @@ -92,7 +92,7 @@ class gallery_Core { $can_add = $item && access::can("add", $item); if ($can_add) { - $menu->append($add_menu = Menu::factory("submenu") + $menu->append($add_menu = Menu::factory("submenu") ->id("add_menu") ->label(t("Add"))); $add_menu->append(Menu::factory("dialog") @@ -100,11 +100,11 @@ class gallery_Core { ->label(t("Add photos")) ->url(url::site("simple_uploader/app/$item->id"))); if ($item->is_album()) { - $add_menu->append(Menu::factory("dialog") + $add_menu->append(Menu::factory("dialog") ->id("add_album_item") ->label(t("Add an album")) ->url(url::site("form/add/albums/$item->id?type=album"))); - } + } } $menu->append($options_menu = Menu::factory("submenu") diff --git a/modules/gallery/helpers/gallery_rss.php b/modules/gallery/helpers/gallery_rss.php index 8e887368..dee6ae40 100644 --- a/modules/gallery/helpers/gallery_rss.php +++ b/modules/gallery/helpers/gallery_rss.php @@ -53,9 +53,9 @@ class gallery_rss_Core { ->descendants($limit, $offset, array("type" => "photo")); $feed->max_pages = ceil( $item->viewable()->descendants_count(array("type" => "photo")) / $limit); - $feed->title = p::purify($item->title); + $feed->title = html::purify($item->title); $feed->link = url::abs_site("albums/{$item->id}"); - $feed->description = nl2br(p::purify($item->description)); + $feed->description = nl2br(html::purify($item->description)); return $feed; } diff --git a/modules/gallery/helpers/gallery_task.php b/modules/gallery/helpers/gallery_task.php index 9edc3acd..c9557324 100644 --- a/modules/gallery/helpers/gallery_task.php +++ b/modules/gallery/helpers/gallery_task.php @@ -64,10 +64,10 @@ class gallery_task_Core { if (!$success) { $ignored[$item->id] = 1; $errors[] = t("Unable to rebuild images for '%title'", - array("title" => p::purify($item->title))); + array("title" => html::purify($item->title))); } else { $errors[] = t("Successfully rebuilt images for '%title'", - array("title" => p::purify($item->title))); + array("title" => html::purify($item->title))); } } diff --git a/modules/gallery/helpers/graphics.php b/modules/gallery/helpers/graphics.php index a20c58dd..787f8dc3 100644 --- a/modules/gallery/helpers/graphics.php +++ b/modules/gallery/helpers/graphics.php @@ -443,7 +443,7 @@ class graphics_Core { if (!module::get_var("gallery", "graphics_toolkit")) { site_status::warning( t("Graphics toolkit missing! Please choose a toolkit", - array("url" => url::site("admin/graphics"))), + array("url" => html::mark_safe(url::site("admin/graphics")))), "missing_graphics_toolkit"); } } diff --git a/modules/gallery/helpers/p.php b/modules/gallery/helpers/p.php deleted file mode 100644 index 862c769b..00000000 --- a/modules/gallery/helpers/p.php +++ /dev/null @@ -1,39 +0,0 @@ - $key_value) { - foreach ($key_value as $key => $value) { - $config->set("$category.$key", $value); - } - } - self::$_purifier = new HTMLPurifier($config); - } - return self::$_purifier->purify($dirty_html); - } -} diff --git a/modules/gallery/libraries/I18n.php b/modules/gallery/libraries/I18n.php index d0531b9a..c3336052 100644 --- a/modules/gallery/libraries/I18n.php +++ b/modules/gallery/libraries/I18n.php @@ -89,6 +89,12 @@ class I18n_Core { /** * Translates a localizable message. + * + * Security: + * The returned string is safe for use in HTML (it contains a safe subset of HTML and + * interpolation parameters are converted to HTML entities). + * For use in JavaScript, please call ->for_js() on it. + * * @param $message String|array The message to be translated. E.g. "Hello world" * or array("one" => "One album", "other" => "%count albums") * @param $options array (optional) Options array for key value pairs which are used @@ -115,7 +121,7 @@ class I18n_Core { $entry = $this->interpolate($locale, $entry, $values); - return $entry; + return SafeString::of_safe_html($entry); } private function lookup($locale, $message) { @@ -184,17 +190,19 @@ class I18n_Core { return is_array($message); } - private function interpolate($locale, $string, $values) { + private function interpolate($locale, $string, $key_values) { // TODO: Handle locale specific number formatting. // Replace x_y before replacing x. - krsort($values, SORT_STRING); + krsort($key_values, SORT_STRING); $keys = array(); - foreach (array_keys($values) as $key) { + $values = array(); + foreach ($key_values as $key => $value) { $keys[] = "%$key"; + $values[] = new SafeString($value); } - return str_replace($keys, array_values($values), $string); + return str_replace($keys, $values, $string); } private function pluralize($locale, $entry, $count) { @@ -419,4 +427,4 @@ class I18n_Core { return $count == 1 ? 'one' : 'other'; } } -} \ No newline at end of file +} diff --git a/modules/gallery/libraries/MY_ORM.php b/modules/gallery/libraries/MY_ORM.php index de8adc1d..2c9ad1d7 100644 --- a/modules/gallery/libraries/MY_ORM.php +++ b/modules/gallery/libraries/MY_ORM.php @@ -43,6 +43,10 @@ class ORM extends ORM_Core { $this->original = clone $this; } + if ($value instanceof SafeString) { + $value = $value->unescaped(); + } + return parent::__set($column, $value); } diff --git a/modules/gallery/libraries/SafeString.php b/modules/gallery/libraries/SafeString.php new file mode 100644 index 00000000..cc542e01 --- /dev/null +++ b/modules/gallery/libraries/SafeString.php @@ -0,0 +1,169 @@ +_is_safe_html = $string->_is_safe_html; + $this->_is_purified_html = $string->_is_purified_html; + $string = $string->unescaped(); + } + $this->_raw_string = (string) $string; + } + + /** + * Factory method returning a new SafeString instance for the given string. + */ + static function of($string) { + return new SafeString($string); + } + + /** + * Factory method returning a new SafeString instance after HTML purifying + * the given string. + */ + static function purify($string) { + if ($string instanceof SafeString) { + $string = $string->unescaped(); + } + $safe_string = self::of_safe_html(self::_purify_for_html($string)); + $safe_string->_is_purified_html = true; + return $safe_string; + } + + /** + * Factory method returning a new SafeString instance which won't HTML escape. + */ + static function of_safe_html($string) { + $safe_string = new SafeString($string); + $safe_string->_is_safe_html = true; + return $safe_string; + } + + /** + * Safe for use in HTML. + * @see #for_html() + */ + function __toString() { + if ($this->_is_safe_html) { + return $this->_raw_string; + } else { + return self::_escape_for_html($this->_raw_string); + } + } + + /** + * Safe for use in HTML. + * + * Example:
    +   *   
    + *
    + * @return the string escaped for use in HTML. + */ + function for_html() { + return $this; + } + + /** + * Safe for use as JavaScript string. + * + * Example:
    +   *   
    +   * 
    + * @return the string escaped for use in JavaScript. + */ + function for_js() { + return json_encode((string) $this->_raw_string); + } + + /** + * Safe for use in HTML element attributes. + * + * Assumes that the HTML element attribute is already + * delimited by single or double quotes + * + * Example:
    +   *     ;
    +   *   
    +   * 
    + * @return the string escaped for use in HTML attributes. + */ + function for_html_attr() { + $string = (string) $this->for_html(); + return strtr($string, + array("'"=>"'", + '"'=>'"')); + } + + /** + * Safe for use HTML (purified HTML) + * + * Example:
    +   *   
    purified_html() ?> + *
    + * @return the string escaped for use in HTML. + */ + function purified_html() { + if ($this->_is_purified_html) { + return $this; + } else { + return self::purify($this); + } + } + + /** + * Returns the raw, unsafe string. Do not use lightly. + */ + function unescaped() { + return $this->_raw_string; + } + + // Escapes special HTML chars ("<", ">", "&", etc.) to HTML entities. + private static function _escape_for_html($dirty_html) { + return html::specialchars($dirty_html); + } + + // Purifies the string, removing any potentially malicious or unsafe HTML / JavaScript. + private static function _purify_for_html($dirty_html) { + if (empty(self::$_purifier)) { + require_once(dirname(__file__) . "/../lib/HTMLPurifier/HTMLPurifier.auto.php"); + $config = HTMLPurifier_Config::createDefault(); + foreach (Kohana::config('purifier') as $category => $key_value) { + foreach ($key_value as $key => $value) { + $config->set("$category.$key", $value); + } + } + self::$_purifier = new HTMLPurifier($config); + } + return self::$_purifier->purify($dirty_html); + } +} diff --git a/modules/gallery/tests/File_Structure_Test.php b/modules/gallery/tests/File_Structure_Test.php index 8a97e00b..9018f4c6 100644 --- a/modules/gallery/tests/File_Structure_Test.php +++ b/modules/gallery/tests/File_Structure_Test.php @@ -177,10 +177,20 @@ class File_Structure_Test extends Unit_Test_Case { new GalleryCodeFilterIterator( new RecursiveIteratorIterator( new RecursiveDirectoryIterator(DOCROOT)))); + $errors = array(); foreach ($dir as $file) { - $this->assert_false( - preg_match('/\t/', file_get_contents($file)), - "{$file->getPathname()} has tabs in it"); + $file_as_string = file_get_contents($file); + if (preg_match('/\t/', $file_as_string)) { + foreach (split("\n", $file_as_string) as $l => $line) { + if (preg_match('/\t/', $line)) { + $errors[] = "$file:$l has tab(s) ($line)"; + } + } + } + $file_as_string = null; + } + if ($errors) { + $this->assert_false(true, "tab(s) found:\n" . join("\n", $errors)); } } diff --git a/modules/gallery/tests/Html_Helper_Test.php b/modules/gallery/tests/Html_Helper_Test.php new file mode 100644 index 00000000..3623705e --- /dev/null +++ b/modules/gallery/tests/Html_Helper_Test.php @@ -0,0 +1,55 @@ +world

    "); + $this->assert_equal("hello <p >world</p>", + $safe_string); + $this->assert_true($safe_string instanceof SafeString); + } + + public function purify_test() { + $safe_string = html::purify("hello

    world

    "); + $this->assert_equal("hello

    world

    ", + $safe_string); + $this->assert_true($safe_string instanceof SafeString); + } + + public function mark_safe_test() { + $safe_string = html::mark_safe("hello

    world

    "); + $this->assert_true($safe_string instanceof SafeString); + $safe_string_2 = html::clean($safe_string); + $this->assert_equal("hello

    world

    ", + $safe_string_2); + } + + public function js_string_test() { + $string = html::js_string("hello's

    world

    "); + $this->assert_equal('"hello\'s

    world<\\/p>"', + $string); + } + + public function clean_attribute_test() { + $safe_string = SafeString::of_safe_html("hello's

    world

    "); + $safe_string = html::clean_attribute($safe_string); + $this->assert_equal("hello's

    world

    ", + $safe_string); + } +} \ No newline at end of file diff --git a/modules/gallery/tests/SafeString_Test.php b/modules/gallery/tests/SafeString_Test.php new file mode 100644 index 00000000..0895b7dd --- /dev/null +++ b/modules/gallery/tests/SafeString_Test.php @@ -0,0 +1,121 @@ +world

    "); + $this->assert_equal("hello <p>world</p>", + $safe_string); + } + + public function toString_for_safe_string_test() { + $safe_string = SafeString::of_safe_html("hello

    world

    "); + $this->assert_equal("hello

    world

    ", + $safe_string); + } + + public function for_html_test() { + $safe_string = new SafeString("hello

    world

    "); + $this->assert_equal("hello <p>world</p>", + $safe_string->for_html()); + } + + public function safestring_of_safestring_test() { + $safe_string = new SafeString("hello

    world

    "); + $safe_string_2 = new SafeString($safe_string); + $this->assert_true($safe_string_2 instanceof SafeString); + $raw_string = $safe_string_2->unescaped(); + $this->assert_false(is_object($raw_string)); + $this->assert_equal("hello

    world

    ", $raw_string); + $this->assert_equal("hello <p>world</p>", $safe_string_2); + } + + public function for_js_test() { + $safe_string = new SafeString('"Foo\'s bar"'); + $js_string = $safe_string->for_js(); + $this->assert_equal('"\\"Foo<\\/em>\'s bar\\""', + $js_string); + } + + public function for_html_attr_test() { + $safe_string = new SafeString('"Foo\'s bar"'); + $attr_string = $safe_string->for_html_attr(); + $this->assert_equal('"<em>Foo</em>'s bar"', + $attr_string); + } + + public function for_html_attr_with_safe_html_test() { + $safe_string = SafeString::of_safe_html('"Foo\'s bar"'); + $attr_string = $safe_string->for_html_attr(); + $this->assert_equal('"Foo's bar"', + $attr_string); + } + + public function string_safestring_equality_test() { + $safe_string = new SafeString("hello

    world

    "); + $this->assert_equal("hello

    world

    ", + $safe_string->unescaped()); + $escaped_string = "hello <p>world</p>"; + $this->assert_equal($escaped_string, $safe_string); + + $this->assert_true($escaped_string == $safe_string); + $this->assert_false($escaped_string === $safe_string); + $this->assert_false("meow" == $safe_string); + } + + public function of_test() { + $safe_string = SafeString::of("hello

    world

    "); + $this->assert_equal("hello

    world

    ", $safe_string->unescaped()); + } + + public function of_safe_html_test() { + $safe_string = SafeString::of_safe_html("hello

    world

    "); + $this->assert_equal("hello

    world

    ", $safe_string->for_html()); + } + + public function purify_test() { + $safe_string = SafeString::purify("hello

    world

    "); + $this->assert_equal("hello

    world

    ", $safe_string); + } + + public function of_fluid_api_test() { + $escaped_string = SafeString::of("Foo's bar")->for_js(); + $this->assert_equal('"Foo\'s bar"', $escaped_string); + } + + public function safestring_of_safestring_preserves_safe_status_test() { + $safe_string = SafeString::of_safe_html("hello's

    world

    "); + $safe_string_2 = new SafeString($safe_string); + $this->assert_equal("hello's

    world

    ", $safe_string_2); + $this->assert_equal('"hello\'s

    world<\\/p>"', $safe_string_2->for_js()); + } + + public function safestring_of_safestring_preserves_html_safe_status_test() { + $safe_string = SafeString::of_safe_html("hello's

    world

    "); + $safe_string_2 = new SafeString($safe_string); + $this->assert_equal("hello's

    world

    ", $safe_string_2); + $this->assert_equal('"hello\'s

    world<\\/p>"', $safe_string_2->for_js()); + } + + public function safestring_of_safestring_safe_status_override_test() { + $safe_string = new SafeString("hello

    world

    "); + $safe_string_2 = SafeString::of_safe_html($safe_string); + $this->assert_equal("hello

    world

    ", $safe_string_2); + } +} diff --git a/modules/gallery/tests/Xss_Security_Test.php b/modules/gallery/tests/Xss_Security_Test.php index 9bde11dc..6c141c52 100644 --- a/modules/gallery/tests/Xss_Security_Test.php +++ b/modules/gallery/tests/Xss_Security_Test.php @@ -19,87 +19,336 @@ */ class Xss_Security_Test extends Unit_Test_Case { public function find_unescaped_variables_in_views_test() { + $found = array(); foreach (glob("*/*/views/*.php") as $view) { - $expr = null; - $level = 0; - $php = 0; - $str = null; - $in_p_clean = 0; + // List of all tokens without whitespace, simplifying parsing. + $tokens = array(); foreach (token_get_all(file_get_contents($view)) as $token) { - if (false /* useful for debugging */) { - if (is_array($token)) { - printf("[$str] [$in_p_clean] %-15s %s\n", token_name($token[0]), $token[1]); - } else { - printf("[$str] [$in_p_clean] %-15s %s\n", "", $token); + if (!is_array($token) || ($token[0] != T_WHITESPACE)) { + $tokens[] = $token; + } + } + + $frame = null; + $script_block = 0; + $in_script_block = false; + + for ($token_number = 0; $token_number < count($tokens); $token_number++) { + $token = $tokens[$token_number]; + + // Are we in a block? + if (is_array($token) && $token[0] == T_INLINE_HTML) { + $inline_html = $token[1]; + // T_INLINE_HTML blocks can be split. Need to handle the case + // where one token has "expr_append($inline_html); + } + + // Note: This approach won't catch }i', $inline_html, $matches, PREG_OFFSET_CAPTURE)) { + $last_match = array_pop($matches[0]); + if (is_array($last_match)) { + $closing_script_pos = $last_match[1]; + } else { + $closing_script_pos = $last_match; + } + } + if (preg_match('{]*>}i', $inline_html, $matches, PREG_OFFSET_CAPTURE)) { + $last_match = array_pop($matches[0]); + if (is_array($last_match)) { + $opening_script_pos = $last_match[1]; + } else { + $opening_script_pos = $last_match; + } + } + if ($opening_script_pos != $closing_script_pos) { + $in_script_block = $opening_script_pos > $closing_script_pos; } } - // If we find a "(" after a "p::clean" then start counting levels of parens and assume - // that we're inside a p::clean() call until we find the matching close paren. - if ($token[0] == "(" && ($str == "p::clean" || $str == "p::purify")) { - $in_p_clean = 1; - } else if ($token[0] == "(" && $in_p_clean) { - $in_p_clean++; - } else if ($token[0] == ")" && $in_p_clean) { - $in_p_clean--; - } - - // Concatenate runs of strings for convenience, which we use above to figure out if we're - // inside a p::clean() call or not - if ($token[0] == T_STRING || $token[0] == T_DOUBLE_COLON) { - $str .= $token[1]; - } else { - $str = null; - } - - // Scan for any occurrences of < ? = $variable ? > and store it in $expr - if ($token[0] == T_OPEN_TAG_WITH_ECHO) { - $php++; - } else if ($php && $token[0] == T_CLOSE_TAG) { - $php--; - } else if ($php && $token[0] == T_VARIABLE) { - if (!$expr) { - $entry = array($token[2], $in_p_clean); + // Look and report each instance of < ? = ... ? > + if (!is_array($token)) { + // A single char token, e.g: ; ( ) + if ($frame) { + $frame->expr_append($token); } - $expr .= $token[1]; - } else if ($expr) { - if ($token[0] == T_OBJECT_OPERATOR) { - $expr .= $token[1]; - } else if ($token[0] == T_STRING) { - $expr .= $token[1]; - } else if ($token == "(") { - $expr .= $token; - $level++; - } else if ($level > 0 && $token == ")") { - $expr .= $token; - $level--; - } else if ($level > 0) { - $expr .= is_array($token) ? $token[1] : $token; - } else { - $entry[] = $expr; - $found[$view][] = $entry; - $expr = null; - $entry = null; + } else if ($token[0] == T_OPEN_TAG_WITH_ECHO) { + // No need for a stack here - assume < ? = cannot be nested. + $frame = self::_create_frame($token, $in_script_block); + } else if ($frame && $token[0] == T_CLOSE_TAG) { + // Store the < ? = ... ? > block that just ended here. + $found[$view][] = $frame; + $frame = null; + } else if ($frame && $token[0] == T_VARIABLE) { + $frame->expr_append($token[1]); + if ($token[1] == '$theme') { + if (self::_token_matches(array(T_OBJECT_OPERATOR, "->"), $tokens, $token_number + 1) && + self::_token_matches(array(T_STRING), $tokens, $token_number + 2) && + in_array($tokens[$token_number + 2][1], + array("thumb_proportion", "site_menu", "album_menu", "tag_menu", "photo_menu", + "context_menu", "pager", "site_status", "messages", "album_blocks", + "album_bottom", "album_top", "body_attributes", "credits", + "dynamic_bottom", "dynamic_top", "footer", "head", "header_bottom", + "header_top", "page_bottom", "page_top", "photo_blocks", "photo_bottom", + "photo_top", "resize_bottom", "resize_top", "sidebar_blocks", "sidebar_bottom", + "sidebar_top", "thumb_bottom", "thumb_info", "thumb_top")) && + self::_token_matches("(", $tokens, $token_number + 3)) { + + $method = $tokens[$token_number + 2][1]; + $frame->expr_append("->$method("); + + $token_number += 3; + $token = $tokens[$token_number]; + + $frame->is_safe_html(true); + } else if (self::_token_matches(array(T_OBJECT_OPERATOR, "->"), $tokens, $token_number + 1) && + self::_token_matches(array(T_STRING), $tokens, $token_number + 2) && + in_array($tokens[$token_number + 2][1], + array("css", "script", "url")) && + self::_token_matches("(", $tokens, $token_number + 3) && + // Only allow constant strings here + self::_token_matches(array(T_CONSTANT_ENCAPSED_STRING), $tokens, $token_number + 4)) { + + $method = $tokens[$token_number + 2][1]; + $frame->expr_append("->$method("); + + $token_number += 4; + $token = $tokens[$token_number]; + + $frame->is_safe_html(true); + } } + } else if ($frame && $token[0] == T_STRING) { + $frame->expr_append($token[1]); + // t() and t2() are special in that they're guaranteed to return a SafeString(). + if (in_array($token[1], array("t", "t2"))) { + if (self::_token_matches("(", $tokens, $token_number + 1)) { + $frame->is_safe_html(true); + $frame->expr_append("("); + + $token_number++; + $token = $tokens[$token_number]; + } + } else if ($token[1] == "SafeString") { + // Looking for SafeString::of(... + if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) && + self::_token_matches(array(T_STRING), $tokens, $token_number + 2) && + in_array($tokens[$token_number + 2][1], array("of", "purify")) && + self::_token_matches("(", $tokens, $token_number + 3)) { + // Not checking for of_safe_html(). We want such calls to be marked dirty (thus reviewed). + + $frame->is_safe_html(true); + + $method = $tokens[$token_number + 2][1]; + $frame->expr_append("::$method("); + + $token_number += 3; + $token = $tokens[$token_number]; + } + } else if ($token[1] == "json_encode") { + if (self::_token_matches("(", $tokens, $token_number + 1)) { + $frame->is_safe_js(true); + $frame->expr_append("("); + + $token_number++; + $token = $tokens[$token_number]; + } + } else if ($token[1] == "url") { + // url methods return safe HTML + if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) && + self::_token_matches(array(T_STRING), $tokens, $token_number + 2) && + in_array($tokens[$token_number + 2][1], + array("site", "current", "base", "file", "abs_site", "abs_current", + "abs_file", "merge")) && + self::_token_matches("(", $tokens, $token_number + 3)) { + $frame->is_safe_html(true); + + $method = $tokens[$token_number + 2][1]; + $frame->expr_append("::$method("); + + $token_number += 3; + $token = $tokens[$token_number]; + } + } else if ($token[1] == "html") { + if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) && + self::_token_matches(array(T_STRING), $tokens, $token_number + 2) && + in_array($tokens[$token_number + 2][1], + array("clean", "purify", "js_string", "clean_attribute")) && + self::_token_matches("(", $tokens, $token_number + 3)) { + // Not checking for mark_safe(). We want such calls to be marked dirty (thus reviewed). + + $method = $tokens[$token_number + 2][1]; + $frame->expr_append("::$method("); + + $token_number += 3; + $token = $tokens[$token_number]; + + if ("js_string" == $method) { + $frame->is_safe_js(true); + } else { + $frame->is_safe_html(true); + } + } + } + } else if ($frame && $token[0] == T_OBJECT_OPERATOR) { + $frame->expr_append($token[1]); + + if (self::_token_matches(array(T_STRING), $tokens, $token_number + 1) && + in_array($tokens[$token_number + 1][1], + array("for_js", "for_html", "purified_html", "for_html_attr")) && + self::_token_matches("(", $tokens, $token_number + 2)) { + $method = $tokens[$token_number + 1][1]; + $frame->expr_append("$method("); + + $token_number += 2; + $token = $tokens[$token_number]; + + if ("for_js" == $method) { + $frame->is_safe_js(true); + } else { + $frame->is_safe_html(true); + } + } + } else if ($frame) { + $frame->expr_append($token[1]); } } } - $canonical = MODPATH . "gallery/tests/xss_data.txt"; + /* + * Generate the report + * + * States for uses of < ? = X ? >: + * DIRTY_JS: + * In
    diff --git a/modules/gallery/views/move_tree.html.php b/modules/gallery/views/move_tree.html.php index 5f70cf67..623f80ee 100644 --- a/modules/gallery/views/move_tree.html.php +++ b/modules/gallery/views/move_tree.html.php @@ -1,18 +1,18 @@ thumb_img(array(), 25); ?> is_descendant($parent)): ?> - title) ?> + title) ?> - title) ?> + title) ?>