Содержание

[08.03.10] XSS уязвимость в BB-кодах acronym

Для исправления уязвимостей Вы можете воспользоваться готовыми файлами. В архиве сохранена структура файлов для простоты использования. Делать это стоит только в том случае, если не было произведено никаких изменений в этих файлах.

Либо воспользоваться инструкциями приведенными ниже.

XSS уязвимость в BB-коде acronym

Злоумышленник может сформировать вложенный bb-тег, на основе bb-кода acronym, позволяющий внедрить собственный HTML код в финальную страницу, отдаваемую форумом. Опасность данной уязвимости не значительна, если Ваши форумы используют httpOnly cookies.

Исправление

В файле admin/applications/members/extensions/usercpForms.php найти:

	/**
	 * Custom event: Remove avatar
	 *
	 * @access	public
	 * @author	Matt Mecham
	 * @return	string		Processed HTML
	 */
	public function customEvent_removeAvatar()

Добавить после:

		//-----------------------------------------
		// Check secure hash...
		//-----------------------------------------
 
		if ( $this->request['secure_key'] != $this->member->form_hash )
		{
			$this->registry->output->showError( 'authorization_error', 100, true, null, 403 );
		}

Найти:

	/**
	 * Custom event: Remove photo
	 *
	 * @access	public
	 * @author	Matt Mecham
	 * @return	string		Processed HTML
	 */
	function customEvent_removePhoto()
	{

Добавить после:

		//-----------------------------------------
		// Check secure hash...
		//-----------------------------------------
 
		if ( $this->request['secure_key'] != $this->member->form_hash )
		{
			$this->registry->output->showError( 'authorization_error', 100, true, null, 403 );
		}

Найти:

		//-----------------------------------------
		// Reset forced form action?
		//-----------------------------------------
 
		if ( $is_reset )
		{
			$this->settings['base_url'] = $original;
		}

Добавить после:

		/* fix url */
		$return = preg_replace( "#(area=removeavatar(&|&)do=remove)#", "\\1&secure_key=" . $this->member->form_hash, $return );

Найти:

		$this->uploadFormMax = 5000000;
 
 		return $this->registry->getClass('output')->getTemplate('ucp')->membersPhotoForm( $cur_photo, $show_size, $p_max, 500000, $rand );

Заменить на:

		$this->uploadFormMax = 5000000;
 
 		$return = $this->registry->getClass('output')->getTemplate('ucp')->membersPhotoForm( $cur_photo, $show_size, $p_max, 500000, $rand );
 
 		/* fix url */
		$return = preg_replace( "#(area=removephoto(&|&)do=remove)#", "\\1&secure_key=" . $this->member->form_hash, $return );
 
		return $return;

В файле admin/sources/classes/bbcode/core.php найти:

	/**
	 * Check against XSS
	 *
	 * @access	public
	 * @param	string		Original string
	 * @param	boolean		Fix script HTML tags
	 * @return	string		"Cleaned" text
	 */
	public function checkXss( $txt='', $fixScript=false )
	{
		//-----------------------------------------
		// Opening script tags...
		// Check for spaces and new lines...
		//-----------------------------------------
 
		if ( $fixScript )
		{
			$txt = preg_replace( "#<(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is"        , "&lt;script" , $txt );
			$txt = preg_replace( "#<(\s+?)?/(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is", "&lt;/script", $txt );
		}
 
		//-----------------------------------------
		// Here we can do some generic checking for XSS
		// This should not be considered fool proof, though can provide
		//	a centralized point for maintenance and checking
		//-----------------------------------------
 
		$txt = preg_replace( "/(j)avascript/i" , "\\1&#097;v&#097;script", $txt );
		//$txt = str_ireplace( "alert"      , "&#097;lert"          , $txt );
		$txt = str_ireplace( "behavior"   , "beh&#097;vior"    	  , $txt );
		$txt = preg_replace( "/(e)((\/\*.*?\*\/)*)x((\/\*.*?\*\/)*)p((\/\*.*?\*\/)*)r((\/\*.*?\*\/)*)e((\/\*.*?\*\/)*)s((\/\*.*?\*\/)*)s((\/\*.*?\*\/)*)i((\/\*.*?\*\/)*)o((\/\*.*?\*\/)*)n/is" , "\\1xp<b></b>ressi&#111;n"     , $txt );
		$txt = preg_replace( "/(e)((\\\|&#092;)*)x((\\\|&#092;)*)p((\\\|&#092;)*)r((\\\|&#092;)*)e((\\\|&#092;)*)s((\\\|&#092;)*)s((\\\|&#092;)*)i((\\\|&#092;)*)o((\\\|&#092;)*)n/is" 	  , "\\1xp<b></b>ressi&#111;n"     	  , $txt );
		$txt = preg_replace( "/m((\\\|&#092;)*)o((\\\|&#092;)*)z((\\\|&#092;)*)\-((\\\|&#092;)*)b((\\\|&#092;)*)i((\\\|&#092;)*)n((\\\|&#092;)*)d((\\\|&#092;)*)i((\\\|&#092;)*)n((\\\|&#092;)*)g/is" 	  , "moz-<b></b>b&#105;nding"     	  , $txt );
		$txt = str_ireplace( "about:"     , "&#097;bout:"         , $txt );
		$txt = str_ireplace( "<body"      , "&lt;body"            , $txt );
		$txt = str_ireplace( "<html"      , "&lt;html"            , $txt );
		$txt = str_ireplace( "document." , "&#100;ocument."      , $txt );
		$txt = str_ireplace( "window."   , "wind&#111;w."      , $txt );
 
		$event_handlers	= array( 'mouseover', 'mouseout', 'mouseup', 'mousemove', 'mousedown', 'mouseenter', 'mouseleave', 'mousewheel',
								 'contextmenu', 'click', 'dblclick', 'load', 'unload', 'submit', 'blur', 'focus', 'resize', 'scroll',
								 'change', 'reset', 'select', 'selectionchange', 'selectstart', 'start', 'stop', 'keydown', 'keyup',
								 'keypress', 'abort', 'error', 'dragdrop', 'move', 'moveend', 'movestart', 'activate', 'afterprint',
								 'afterupdate', 'beforeactivate', 'beforecopy', 'beforecut', 'beforedeactivate', 'beforeeditfocus',
								 'beforepaste', 'beforeprint', 'beforeunload', 'begin', 'bounce', 'cellchange', 'controlselect',
								 'copy', 'cut', 'paste', 'dataavailable', 'datasetchanged', 'datasetcomplete', 'deactivate', 'drag',
								 'dragend', 'dragleave', 'dragenter', 'dragover', 'drop', 'end', 'errorupdate', 'filterchange', 'finish',
								 'focusin', 'focusout', 'help', 'layoutcomplete', 'losecapture', 'mediacomplete', 'mediaerror', 'outofsync',
								 'pause', 'propertychange', 'progress', 'readystatechange', 'repeat', 'resizeend', 'resizestart', 'resume',
								 'reverse', 'rowsenter', 'rowexit', 'rowdelete', 'rowinserted', 'seek', 'syncrestored', 'timeerror',
								 'trackchange', 'urlflip',
								);
 
		foreach( $event_handlers as $handler )
		{
			$txt = str_ireplace( 'on' . $handler, '&#111;n' . $handler, $txt );
		}
 
		return $txt;
	}

Заменить на:

	public function checkXss( $txt='', $fixScript=false, $tag='' )
	{
		//-----------------------------------------
		// Opening script tags...
		// Check for spaces and new lines...
		//-----------------------------------------
 
		if ( $fixScript )
		{
			$txt = preg_replace( "#<(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is"        , "&lt;script" , $txt );
			$txt = preg_replace( "#<(\s+?)?/(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is", "&lt;/script", $txt );
		}
 
		/* got a tag? */
		if ( $tag )
		{
			$tag = strip_tags( $tag, '<br>' );
 
			switch ($tag)
			{
				case 'entry':
				case 'blog':
				case 'topic':
				case 'post':
					$test = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $txt );
					if ( ! is_numeric( $test ) )
					{
						$txt = false;
					}
				break;
				case 'acronym':
					$test  = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $txt );
					$test1 = IPSText::alphanumericalClean( $test, '.+ ' );
					if ( $test != $test1 )
					{
						$txt = false;
					}
				break;
				case 'email':
					$test = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $txt );
					$test = ( IPSText::checkEmailAddress( $test ) ) ? $txt : FALSE;
				break;
				case 'font':
				case 'color':
				case 'background':
					/* Make sure it's clean */
					$test = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $txt );
					$test1 = IPSText::alphanumericalClean( $test, '#.+, ' );
					if ( $test != $test1 )
					{
						$txt = false;
					}
				break;
			}
 
 
			/* If we didn't actually get any option data, then return false */
			$test = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $txt );
 
			if ( strlen($txt) AND strlen( $test ) < 1 )
			{
				$txt = false;
			}
 
			if ( $txt === false )
			{
				return false;
			}
 
			/* Still here? Safety, then */
			$txt	= strip_tags( $txt, '<br>' );
 
			if( strpos( $txt, '[' ) !== false OR strpos( $txt, ']' ) !== false )
			{
				$txt	= str_replace( array( '[', ']' ), array( '&#91;', '&#93;' ), $txt );
			}
		}
 
		//-----------------------------------------
		// Here we can do some generic checking for XSS
		// This should not be considered fool proof, though can provide
		//	a centralized point for maintenance and checking
		//-----------------------------------------
 
		$txt = preg_replace( "/(j)avascript/i" , "\\1&#097;v&#097;script", $txt );
		//$txt = str_ireplace( "alert"      , "&#097;lert"          , $txt );
		$txt = str_ireplace( "behavior"   , "beh&#097;vior"    	  , $txt );
		$txt = preg_replace( "/(e)((\/\*.*?\*\/)*)x((\/\*.*?\*\/)*)p((\/\*.*?\*\/)*)r((\/\*.*?\*\/)*)e((\/\*.*?\*\/)*)s((\/\*.*?\*\/)*)s((\/\*.*?\*\/)*)i((\/\*.*?\*\/)*)o((\/\*.*?\*\/)*)n/is" , "\\1xp<b></b>ressi&#111;n"     , $txt );
		$txt = preg_replace( "/(e)((\\\|&#092;)*)x((\\\|&#092;)*)p((\\\|&#092;)*)r((\\\|&#092;)*)e((\\\|&#092;)*)s((\\\|&#092;)*)s((\\\|&#092;)*)i((\\\|&#092;)*)o((\\\|&#092;)*)n/is" 	  , "\\1xp<b></b>ressi&#111;n"     	  , $txt );
		$txt = preg_replace( "/m((\\\|&#092;)*)o((\\\|&#092;)*)z((\\\|&#092;)*)\-((\\\|&#092;)*)b((\\\|&#092;)*)i((\\\|&#092;)*)n((\\\|&#092;)*)d((\\\|&#092;)*)i((\\\|&#092;)*)n((\\\|&#092;)*)g/is" 	  , "moz-<b></b>b&#105;nding"     	  , $txt );
		$txt = str_ireplace( "about:"     , "&#097;bout:"         , $txt );
		$txt = str_ireplace( "<body"      , "&lt;body"            , $txt );
		$txt = str_ireplace( "<html"      , "&lt;html"            , $txt );
		$txt = str_ireplace( "document." , "&#100;ocument."      , $txt );
		$txt = str_ireplace( "window."   , "wind&#111;w."      , $txt );
 
		$event_handlers	= array( 'mouseover', 'mouseout', 'mouseup', 'mousemove', 'mousedown', 'mouseenter', 'mouseleave', 'mousewheel',
								 'contextmenu', 'click', 'dblclick', 'load', 'unload', 'submit', 'blur', 'focus', 'resize', 'scroll',
								 'change', 'reset', 'select', 'selectionchange', 'selectstart', 'start', 'stop', 'keydown', 'keyup',
								 'keypress', 'abort', 'error', 'dragdrop', 'move', 'moveend', 'movestart', 'activate', 'afterprint',
								 'afterupdate', 'beforeactivate', 'beforecopy', 'beforecut', 'beforedeactivate', 'beforeeditfocus',
								 'beforepaste', 'beforeprint', 'beforeunload', 'begin', 'bounce', 'cellchange', 'controlselect',
								 'copy', 'cut', 'paste', 'dataavailable', 'datasetchanged', 'datasetcomplete', 'deactivate', 'drag',
								 'dragend', 'dragleave', 'dragenter', 'dragover', 'drop', 'end', 'errorupdate', 'filterchange', 'finish',
								 'focusin', 'focusout', 'help', 'layoutcomplete', 'losecapture', 'mediacomplete', 'mediaerror', 'outofsync',
								 'pause', 'propertychange', 'progress', 'readystatechange', 'repeat', 'resizeend', 'resizestart', 'resume',
								 'reverse', 'rowsenter', 'rowexit', 'rowdelete', 'rowinserted', 'seek', 'syncrestored', 'timeerror',
								 'trackchange', 'urlflip',
								);
 
		foreach( $event_handlers as $handler )
		{
			$txt = str_ireplace( 'on' . $handler, '&#111;n' . $handler, $txt );
		}
 
		return $txt;
	}

Найти:

						//-----------------------------------------
						// If this is a single tag, that's it
						//-----------------------------------------
 
						if( $_bbcode['bbcode_single_tag'] )
						{
							$txt = substr_replace( $txt, $this->_bbcodeToHtml( $_bbcode, $_option, '' ), $this->cur_pos, ($open_length + strlen($_option) + 1) );
						}
 
						//-----------------------------------------
						// Otherwise replace out the content too
						//-----------------------------------------
 
						else
						{
							$close_tag	= '[/' . $_tag . ']';
 
							if( stripos( $txt, $close_tag, $new_pos ) !== false )
							{
								$_content	= substr( $txt, ($this->cur_pos + $open_length + strlen($_option) + 1), (stripos( $txt, $close_tag, $this->cur_pos ) - ($this->cur_pos + $open_length + strlen($_option) + 1)) );
								$txt		= substr_replace( $txt, $this->_bbcodeToHtml( $_bbcode, $_option /*? $_option : $_content*/, $_content ), $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );	
							}
							else
							{
								//-----------------------------------------
								// If there's no close tag, no need to continue
								//-----------------------------------------
 
								break;
							}
						}

Заменить на:

						//-----------------------------------------
						// Protect against XSS
						//-----------------------------------------
 
						$_optionStrLen = IPSText::mbstrlen( $_option );
						$_option	   = $this->checkXss($_option, false, $_tag);
 
						if ( $_option !== FALSE )
						{
							//-----------------------------------------
							// If this is a single tag, that's it
							//-----------------------------------------
 
							if( $_bbcode['bbcode_single_tag'] )
							{
								$txt = substr_replace( $txt, $this->_bbcodeToHtml( $_bbcode, $_option, '' ), $this->cur_pos, ($open_length + $_optionStrLen + 1) );
							}
 
							//-----------------------------------------
							// Otherwise replace out the content too
							//-----------------------------------------
 
							else
							{
								$close_tag	= '[/' . $_tag . ']';
 
								if( stripos( $txt, $close_tag, $new_pos ) !== false )
								{
									$_content	= substr( $txt, ($this->cur_pos + $open_length + $_optionStrLen + 1), (stripos( $txt, $close_tag, $this->cur_pos ) - ($this->cur_pos + $open_length + $_optionStrLen + 1)) );
									$txt		= substr_replace( $txt, $this->_bbcodeToHtml( $_bbcode, $_option /*? $_option : $_content*/, $_content ), $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );	
								}
								else
								{
									//-----------------------------------------
									// If there's no close tag, no need to continue
									//-----------------------------------------
 
									break;
								}
							}
						}

В файле admin/sources/classes/bbcode/custom/defaults.php найти:

class bbcode_quote extends bbcode_parent_class implements bbcodePlugin
{
	/**
	 * Constructor
	 *
	 * @access	public
	 * @param	object		Registry object
	 * @return	void
	 */
	public function __construct( ipsRegistry $registry )
	{
		$this->currentBbcode	= 'quote';
 
		parent::__construct( $registry );
	}
 
	/**
	 * Method that is run before the content is stored in the database
	 * You are responsible for ensuring you mark the replaced text appropriately so that you
	 *	are able to unparse it, if you wish to have bbcode parsed on save
	 *
	 * @access	public
	 * @param	string		$txt	BBCode text from submission to be stored in database
	 * @return	string				Formatted content, ready for display
	 */
	public function preDbParse( $txt )
	{
		$this->cache->updateCacheWithoutSaving( '_tmp_bbcode_quotes', 0 );
 
		return parent::preDbParse( $txt );
	}
 
	/**
	 * Method that is run before the content is displayed to the user
	 * This is the safest method of parsing, as the original submitted text is left in tact.
	 *	No markers are necessary if you use parse on display.
	 *
	 * @access	public
	 * @param	string		$txt	BBCode/parsed text from database to be displayed
	 * @return	string				Formatted content, ready for display
	 */
	public function preDisplayParse( $txt )
	{
		$this->cache->updateCacheWithoutSaving( '_tmp_bbcode_quotes', 0 );
 
		return parent::preDisplayParse( $txt );
	}
 
	/**
	 * Do the actual replacement
	 *
	 * @access	protected
	 * @param	string		$txt	Parsed text from database to be edited
	 * @return	string				BBCode content, ready for editing
	 */
	protected function _replaceText( $txt )
	{
		$_tags = $this->_retrieveTags();
 
		foreach( $_tags as $_tag )
		{
			//-----------------------------------------
			// Determine open and close tags
			//-----------------------------------------
 
			$open_tag	= '[' . $_tag;
			$close_tag	= '[/' . $_tag . ']';
 
			//-----------------------------------------
			// Ok, quotes suck A LOT
			// We have to first match the CLOSE tag, unlike other bbcode
			// Then we back-track to find the matching opening tag
			// Do the replacement, and repeat....
			//-----------------------------------------
 
			while( ( $this->end_pos = stripos( $txt, $close_tag, $this->end_pos ) ) !== false )
			{
				//-----------------------------------------
				// Ok at this point, we have a closing quote tag
				// Set start position to end position point
				//-----------------------------------------
 
				$this->cur_pos	= $this->end_pos;
 
				//-----------------------------------------
				// We are at 0 or above still...
				//-----------------------------------------
 
				while( $this->cur_pos > 0 )
				{
					//-----------------------------------------
					// Loop over characters moving backwards 1 by 1
					//-----------------------------------------
 
					$this->cur_pos = $this->cur_pos - 1;
 
					//-----------------------------------------
					// Shouldn't hit this, but if we do we're done
					//-----------------------------------------
 
					if( $this->cur_pos < 0 )
					{
						break;
					}
 
					//-----------------------------------------
					// Have we finally hit the first preceeding open tag?
					//-----------------------------------------
 
					if( stripos( $txt, $open_tag, $this->cur_pos ) === $this->cur_pos )
					{
						//-----------------------------------------
						// Start figuring out open tag length
						//-----------------------------------------
 
						$open_length	= strlen($open_tag);
 
						//-----------------------------------------
						// Extract the options (like surgery)
						//-----------------------------------------
 
						$_option		= '';
						$quoteOptions	= array();
 
						//-----------------------------------------
						// Does we haz it?
						//-----------------------------------------
 
						if( substr( $txt, $this->cur_pos + strlen($open_tag), 1 ) == ' ' )
						{
							$open_length	+= 1;
							$_option		= substr( $txt, $this->cur_pos + $open_length, (strpos( $txt, ']', $this->cur_pos ) - ($this->cur_pos + $open_length)) );
							$quoteOptions	= $this->_extractSurgicallyTehOptions( $_option );
						}
 
						//-----------------------------------------
						// If not, [u] != [url] (for example)
						//-----------------------------------------
 
						else if( (strpos( $txt, ']', $this->cur_pos ) - ( $this->cur_pos + $open_length )) !== 0 )
						{
							continue;
						}
 
						//-----------------------------------------
						// Otherwise replace out the content too
						//-----------------------------------------
 
						$_content	= substr( $txt, ($this->cur_pos + $open_length + strlen($_option) + 1), (stripos( $txt, $close_tag, $this->cur_pos ) - ($this->cur_pos + $open_length + strlen($_option) + 1)) );
 
						//-----------------------------------------
						// Turn attachments into links
						// Prevents em from breaking on other pages
						//-----------------------------------------
 
						preg_match_all( "#\[attachment=(.+?):(.+?)\]#", $_content, $matches );
 
						if( is_array( $matches[1] ) && count( $matches[1] ) )
						{
							foreach( $matches[1] as $idx => $attach_id )
							{
								$_content = str_replace( "[attachment={$attach_id}:{$matches[2][$idx]}]", $this->registry->getClass('output')->getReplacement('post_attach_link') . " <a href='{$this->settings['base_url']}app=core&amp;module=attach&amp;section=attach&amp;attach_rel_module=post&amp;attach_id={$attach_id}' target='_blank'>{$matches[2][$idx]}</a>", $_content );
							}
						}
 
						//-----------------------------------------
						// Trim off leading newlines / brs, etc
						//-----------------------------------------
 
						$_content = IPSText::br2nl( $_content );
						$_content = trim( $_content );
						$_content = nl2br( $_content );
 
						//-----------------------------------------
						// And replace...
						//-----------------------------------------
 
						$txt = substr_replace( $txt, $this->_buildOutput( $_content, $quoteOptions ? $quoteOptions : '' ), $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );
 
						//-----------------------------------------
						// Now we break since we've done the inner replacement
						//-----------------------------------------
 
						break;
					}
				}
 
				//-----------------------------------------
				// And reset end position to one char further
				//-----------------------------------------
 
				$this->end_pos += 1;
 
				//-----------------------------------------
				// If end_pos is greater than length of text we're done
				//-----------------------------------------
 
				if( $this->end_pos > strlen($txt) )
				{
					//-----------------------------------------
					// Need to reset for next "tag"
					//-----------------------------------------
 
					$this->end_pos	= 0;
					break;
				}
			}
		}
 
		return $txt;
	}
 
	/**
	 * Raw options string
	 *
	 * @access	private
	 * @param	string		$string		The options submitted with the post as a string
	 * @return	array					Key => value Option pairs
	 */
	private function _extractSurgicallyTehOptions( $options='' )
	{
		if( !$options )
		{
			return array();
		}
 
		/**
		 * Strip tags removes any added tags
		 * @see	http://community.invisionpower.com/tracker/issue-19960-problem-when-tags-is-put-inside-quote-starter-tag/
		 */
		$options	= str_replace( '&#39;', "'", strip_tags($options) );
		$options	= str_replace( '&quot;', '"', $options );
 
		$finalOpts	= array();
 
		// Need to push through string, pulling out keys/options
 
		$pos		= 0;
		$options	= trim($options);
		$key		= '';
		$value		= '';
		$inKey		= true;
		$inValue	= false;
 
		while( $pos < strlen($options) )
		{
			if( $options{$pos} == ' ' AND !$inKey AND !$inValue )
			{
				$key				= '';
				$value				= '';
				$inKey				= true;
			}
			else if( $options{$pos} == "'" OR $options{$pos} == '"' )
			{
				if( $inKey )
				{
					$inKey		= false;
					$inValue	= true;
				}
				else
				{
					$inValue	= false;
 
					$finalOpts[ trim($key) ]	= $value;
				}
			}
			else
			{
				if( $inKey )
				{
					if( $options{$pos} != '=' )
					{
						$key .= $options{$pos};
					}
				}
				else if( $inValue )
				{
					$value .= $options{$pos};
				}
			}
 
			$pos++;
		}
 
		return $finalOpts;
	}
 
	/**
	 * Build the actual output to show
	 *
	 * @access	private
	 * @param	string		$content	This is the contents of the quote
	 * @param	array 		$options	[Optional] Quote options
	 * @return	string					Content to replace bbcode with
	 */
	private function _buildOutput( $content, $options=array() )
	{
		//-----------------------------------------
		// Too many quotes?
		//-----------------------------------------
 
		$existing	= $this->cache->getCache('_tmp_bbcode_quotes');
		$existing	= intval($existing) + 1;
 
		if ( $this->settings['max_quotes_per_post'] )
		{
			if ($existing > $this->settings['max_quotes_per_post'])
			{
				$this->error = 'too_many_quotes';
				return $content;
			}
		}
 
		$this->cache->updateCacheWithoutSaving( '_tmp_bbcode_quotes', $existing );
 
		//-----------------------------------------
		// Build output and return it
		//-----------------------------------------
 
		$output	= "<p class='citation'>";
 
		$snapback	= '';
 
		if( $options['post'] )
		{
			$snapback = "<a class='snapback' rel='citation' href='{$this->settings['base_url']}app=forums&amp;module=forums&amp;section=findpost&amp;pid={$options['post']}'>" . 
						$this->registry->output->getReplacement( 'snapback' ) . "</a>";
		}
 
		if( $options['name'] OR $options['date'] OR $options['timestamp'] )
		{
			// sort timestamp
			if ( !$this->settings['cc_on'] AND $options['timestamp'] AND strlen( $options['timestamp'] ) == 10 AND ( intval($options['timestamp']) == $options['timestamp'] ) )
			{
				$options['date'] = $this->registry->getClass('class_localization')->getDate( $options['timestamp'], 'LONG' );
			}
 
			if( $options['name'] AND $options['date'] )
			{
				$output .=  $snapback . sprintf( $this->lang->words['bbc_full_cite'], $options['name'], $options['date'] ) ;
			}
			else if( $options['name'] )
			{
				$output .=  $snapback . sprintf( $this->lang->words['bbc_name_cite'], $options['name'] ) ;
			}
			else if( $options['date'] )
			{
				$output .= $snapback . sprintf( $this->lang->words['bbc_date_cite'], $options['date'] ) ;
			}
		}
		else
		{
			$output .= $snapback . $this->lang->words['bbc_quote'];
		}
 
		$output .= "</p>";
		$output .= '<div class="blockquote"';
 
		$output .= '>';
		$output .= "<div class='quote'>";
 
		$output .= $content;
 
		$output .= "</div>";
		$output .= '</div>';
 
		return $output;
	}
}

Заменить на:

class bbcode_quote extends bbcode_parent_class implements bbcodePlugin
{
	/**#@+
	 * Quote tracking
	 *
	 * @access	protected
	 * @var		int
	 */
	protected $quote_open	= 0;
	protected $quote_closed	= 0;
	protected $quote_error	= 0;
	/**#@-*/
 
	/**
	 * Constructor
	 *
	 * @access	public
	 * @param	object		Registry object
	 * @return	void
	 */
	public function __construct( ipsRegistry $registry )
	{
		$this->currentBbcode	= 'quote';
 
		parent::__construct( $registry );
	}
 
	/**
	 * Method that is run before the content is stored in the database
	 * You are responsible for ensuring you mark the replaced text appropriately so that you
	 *	are able to unparse it, if you wish to have bbcode parsed on save
	 *
	 * @access	public
	 * @param	string		$txt	BBCode text from submission to be stored in database
	 * @return	string				Formatted content, ready for display
	 */
	public function preDbParse( $txt )
	{
		return parent::preDbParse( $txt );
	}
 
	/**
	 * Method that is run before the content is displayed to the user
	 * This is the safest method of parsing, as the original submitted text is left in tact.
	 *	No markers are necessary if you use parse on display.
	 *
	 * @access	public
	 * @param	string		$txt	BBCode/parsed text from database to be displayed
	 * @return	string				Formatted content, ready for display
	 */
	public function preDisplayParse( $txt )
	{
		return parent::preDisplayParse( $txt );
	}
 
	/**
	 * Do the actual replacement
	 *
	 * @access	protected
	 * @param	string		$txt	Parsed text from database to be edited
	 * @return	string				BBCode content, ready for editing
	 */
	protected function _replaceText( $txt )
	{
		$txt	= preg_replace_callback( "#(\[quote([^\]]+?)?\].*\[/quote\])#is" , array( $this, '_parseQuote' ), $txt );
 
		return $txt;
	}
 
	/**
	 * Build the actual output to show
	 *
	 * @access	private
	 * @param	array 		$options	[Optional] Quote options
	 * @return	string					Content to replace bbcode with
	 */
	private function _buildOutput( $options=array() )
	{
		//-----------------------------------------
		// Build output and return it
		//-----------------------------------------
 
		$output	= "<p class='citation'>";
 
		$snapback	= '';
 
		if( $options['post'] )
		{
			$snapback = "<a class='snapback' rel='citation' href='{$this->settings['base_url']}app=forums&amp;module=forums&amp;section=findpost&amp;pid={$options['post']}'>" . 
						$this->registry->output->getReplacement( 'snapback' ) . "</a>";
		}
 
		if( $options['name'] OR $options['date'] OR $options['timestamp'] )
		{
			// sort timestamp
			if ( !$this->settings['cc_on'] AND $options['timestamp'] AND strlen( $options['timestamp'] ) == 10 AND ( intval($options['timestamp']) == $options['timestamp'] ) )
			{
				$options['date'] = $this->registry->getClass('class_localization')->getDate( $options['timestamp'], 'LONG' );
			}
 
			if( $options['name'] AND $options['date'] )
			{
				$output .=  $snapback . sprintf( $this->lang->words['bbc_full_cite'], $options['name'], $options['date'] ) ;
			}
			else if( $options['name'] )
			{
				$output .=  $snapback . sprintf( $this->lang->words['bbc_name_cite'], $options['name'] ) ;
			}
			else if( $options['date'] )
			{
				$output .= $snapback . sprintf( $this->lang->words['bbc_date_cite'], $options['date'] ) ;
			}
		}
		else
		{
			$output .= $snapback . $this->lang->words['bbc_quote'];
		}
 
		$output .= "</p>";
		$output .= '<div class="blockquote"';
 
		$output .= '>';
		$output .= "<div class='quote'>";
 
		return $output;
	}
 
	/**
	 * Callback for quote preg_replace call
	 *
	 * @access	protected
	 * @param	array 		preg_replace_callback Matches
	 * @return	string		Output
	 */
	protected function _parseQuote( $matches )
	{
		$txt	= trim( $matches[1] );
		$_orig	= $txt;
 
		if( !$txt )
		{
			return '';
		}
 
		$this->quote_open   = 0;
		$this->quote_closed = 0;
		$this->quote_error  = 0;
 
		//-----------------------------------------
		// Make sure we don't have too many embedded
		//-----------------------------------------
 
		if ( $this->settings['max_quotes_per_post'] )
		{
			if ( substr_count( strtolower($txt), '[quote' ) > $this->settings['max_quotes_per_post'] )
			{
				$this->error = 'too_many_quotes';
				return $txt;
			}
		}
 
		//-----------------------------------------
		// Fix char 173
		//-----------------------------------------
 
		$txt	= str_replace( chr(173).']', '&#93;', $txt );
 
		//-----------------------------------------
		// Trim the quote content
		//-----------------------------------------
 
		$txt	= preg_replace_callback( "#\[quote([^\]]+?)?\](.+?)\[/quote\]#si", array( $this, '_trimQuote' ), $txt );
 
		//-----------------------------------------
		// Clean usernames with brackets and quotes
		//-----------------------------------------
 
		$txt	= preg_replace_callback( "#(name=(?:&\#39;|&quot;|'|\"))(.+?)(&\#39;|&quot;|'|\")#si", array( $this, '_makeQuoteSafe' ), $txt );
		$txt	= preg_replace_callback( "#name=(&\#39;|&quot;|'|\")(.+?)(\\1)\s?(post|date)=#si", array( $this, '_makeNameSafe' ), $txt );
 
		//-----------------------------------------
		// Replace out end tag
		//-----------------------------------------
 
		$txt	= str_ireplace( "[/quote]", "</div></div>", $txt, $this->quote_closed );
 
		//-----------------------------------------
		// Replace the quote tag
		//-----------------------------------------
 
		$txt	= preg_replace_callback( "#\[quote([^\]]+?)?\]#i", array( $this, '_replaceQuoteTag' ), $txt );
 
		//-----------------------------------------
		// Newlines
		//-----------------------------------------
 
		//$txt	= str_replace( "\n", "<br />", $txt );
 
		$txt	= str_replace( "<div class='quote'><br />", "<div class='quote'>", $txt );
 
		//-----------------------------------------
		// Swap name replacement (_makeNameSafe) back
		//-----------------------------------------
 
		$txt	= str_replace( "&#0039;", "&#39;", $txt );
 
		//-----------------------------------------
		// Turn attachments into links
		// Prevents em from breaking on other pages
		//-----------------------------------------
 
		preg_match_all( "#\[attachment=(.+?):(.+?)\]#", $txt, $_matches );
 
		if( is_array( $_matches[1] ) && count( $_matches[1] ) )
		{
			foreach( $_matches[1] as $idx => $attach_id )
			{
				$txt = str_replace( "[attachment={$attach_id}:{$_matches[2][$idx]}]", $this->registry->getClass('output')->getReplacement('post_attach_link') . " <a href='{$this->settings['base_url']}app=core&amp;module=attach&amp;section=attach&amp;attach_rel_module=post&amp;attach_id={$attach_id}' target='_blank'>{$_matches[2][$idx]}</a>", $txt );
			}
		}
 
		//-----------------------------------------
		// If open and close tags match, we're good.
		// Otherwise, return an error.
		//-----------------------------------------
 
		if ( ( $this->quote_open == $this->quote_closed ) and ( $this->quote_error == 0 ) )
		{
			return $txt;
		}
		else
		{
			$this->error = 'quote_mismatch';
 
			return $_orig;
		}
	}
 
	/**
	 * Callback for triming quote
	 *
	 * @access	protected
	 * @param	array 		preg_replace_callback Matches
	 * @return	string		Output
	 */
	protected function _trimQuote( $matches )
	{
		$txt	= $matches[2];
		$extra	= $matches[1];
 
		if( $txt == "" )
		{
			return "[quote][/quote]";
		}
		else
		{
			$txt = trim( $txt );
 
			return "[quote{$extra}]{$txt}[/quote]";
		}
	}
 
	/**
	 * Make the quoted content safe for regex parsing
	 *
	 * @access	protected
	 * @param	array 		preg_replace_callback Matches
	 * @return	string		Output
	 */
	protected function _makeQuoteSafe( $matches )
	{
		//-----------------------------------------
		// INIT
		//-----------------------------------------
 
		$begin	= $matches[1];
		$end	= $matches[3];
		$txt	= $matches[2];
 
		//-----------------------------------------
		// Sort name
		//-----------------------------------------
 
		$txt = str_replace( "+", "&#043;" , $txt );
		$txt = str_replace( "-", "&#045;" , $txt );
		$txt = str_replace( ":", "&#58;"  , $txt );
		$txt = str_replace( "[", "&#91;"  , $txt );
		$txt = str_replace( "]", "&#93;"  , $txt );
		$txt = str_replace( ")", "&#41;"  , $txt );
		$txt = str_replace( "(", "&#40;"  , $txt );
		$txt = str_replace( "'", "&#039;" , $txt );
 
		return $begin . $txt . $end;
	}
 
	/**
	 * Make the name used for the quote safe for regex parsing
	 *
	 * @access	protected
	 * @param	array 		preg_replace_callback Matches
	 * @return	string		Output
	 */
	protected function _makeNameSafe( $matches )
	{
		$quote	= $matches[1];
		$name	= $matches[2];
		$next	= $matches[4];
 
		# Squeeze past the parser...
		$name  = str_replace( '&#39;', "&#0039;", $name );
 
		return 'name=' . $quote . $name . $quote . ' ' . $next . '='; 
	}
 
	/**
	 * Replace the quote tag
	 *
	 * @access	protected
	 * @param	array 		preg_replace_callback Matches
	 * @return	string		Output
	 */
	protected function _replaceQuoteTag( $matches )
	{
		//-----------------------------------------
		// INIT
		//-----------------------------------------
 
		$extra		= str_replace( '&apos;', "'", $matches[1] );
		$post_id	= 0;
		$date		= '';
		$timestamp	= 0;
		$name		= '';
 
		//-----------------------------------------
		// Inc..
		//-----------------------------------------
 
		$this->quote_open++;
 
		//-----------------------------------------
		// Post?
		//-----------------------------------------
 
		preg_match( "#post=([\"']|&quot;|&\#039;|&\#39;)?(\d+)([\"']|&quot;|&\#039;|&\#39;)?#", $extra, $match );
 
		if ( isset($match[2]) AND intval( $match[2] ) )
		{
			$post_id = intval( $match[2] );
		}
 
		//-----------------------------------------
		// Name?
		//-----------------------------------------
 
		preg_match( "#name=([\"']|&quot;|&\#039;|&\#39;)(.+?)([\"']|&quot;|&\#039;|&\#39;)\s?(date|post)?#is", $extra, $match );
 
		if ( isset($match[2]) AND $match[2] )
		{
			$name = $this->_makeQuoteSafe( array( 2 => $match[2] ) );
		}
 
		//-----------------------------------------
		// Date?
		//-----------------------------------------
 
		preg_match( "#date=([\"']|&quot;|&\#039;|&\#39;)(.+?)([\"']|&quot;|&\#039;|&\#39;)#", $extra, $match );
 
		if ( isset($match[2]) AND $match[2] )
		{
			$date = $this->_makeQuoteSafe( array( 2 => $match[2] ) );
		}
 
		//-----------------------------------------
		// Timestamp?
		//-----------------------------------------
 
		preg_match( "#timestamp=([\"']|&quot;|&\#039;|&\#39;)(.+?)([\"']|&quot;|&\#039;|&\#39;)#", $extra, $match );
 
		if ( isset($match[2]) AND $match[2] )
		{
			$timestamp	= intval( $match[2] );
		}
 
		return $this->_buildOutput( array( 'name' => $name, 'date' => $date, 'post' => $post_id, 'timestamp' => $timestamp ) );
	}
}

Найти:

				//-----------------------------------------
				// No closing tag
				//-----------------------------------------
 
				if( stripos( $txt, $close_tag, $new_pos ) === false )
				{
					break;
				}
 
				$_content	= substr( $txt, ($this->cur_pos + strlen($open_tag) + strlen($_option) + 1), (stripos( $txt, $close_tag, $this->cur_pos ) - ($this->cur_pos + strlen($open_tag) + strlen($_option) + 1)) );
 
				//-----------------------------------------
				// If this is a single tag, that's it
				//-----------------------------------------
 
				if( $_content )
				{
					$txt		= substr_replace( $txt, $this->_buildOutput( $_option, $_content ), $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );
				}
				else
				{
					$txt		= substr_replace( $txt, '', $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );
				}

Заменить на:

				//-----------------------------------------
				// Protect against XSS
				//-----------------------------------------
 
				/* Make sure it's clean */
				$test = str_replace( array( '"', "'", '&quot;', '&#39;' ), "", $_option );
				$test1 = IPSText::alphanumericalClean( $test, '.+ ' );
 
				if ( $test1 != $test )
				{
					$_option = false;
				}
 
				//-----------------------------------------
				// No closing tag
				//-----------------------------------------
 
				if ( $_option !== false AND stripos( $txt, $close_tag, $new_pos ) !== false )
				{
					$_content	= substr( $txt, ($this->cur_pos + strlen($open_tag) + strlen($_option) + 1), (stripos( $txt, $close_tag, $this->cur_pos ) - ($this->cur_pos + strlen($open_tag) + strlen($_option) + 1)) );
 
					//-----------------------------------------
					// If this is a single tag, that's it
					//-----------------------------------------
 
					if( $_content )
					{
						$txt		= substr_replace( $txt, $this->_buildOutput( $_option, $_content ), $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );
					}
					else
					{
						$txt		= substr_replace( $txt, '', $this->cur_pos, (stripos( $txt, $close_tag, $this->cur_pos ) + strlen($close_tag) - $this->cur_pos) );
					}
				}

После установки исправления обязательно сделайте перестроение сообщений/подписей/личных сообщений через админ-центр форума.