Внедрение в строковые параметры
Предположим, серверное ПО, получив запрос на поиск данных в новостях параметром search_text, использует его в следующем SQL-запросе (здесь параметры экранируются кавычками):
…$search_text = $_REQUEST['search_text'];$res = mysql_query("SELECT id_news, news_date, news_caption, news_text, news_id_author FROM news WHERE news_caption LIKE('%$search_text%')");
Сделав запрос вида http://example.org/script.php?search_text=Test мы получим выполнение следующего SQL-запроса:
SELECT id_news, news_date, news_caption, news_text, news_id_author FROM news WHERE news_caption LIKE('%Test%')Но, внедрив в параметр search_text символ кавычки (который используется в запросе), мы можем кардинально изменить поведение SQL-запроса. Например, передав в качестве параметра search_text значение ')+and+(news_id_author='1, мы вызовем к выполнению запрос:
SELECT id_news, news_date, news_caption, news_text, news_id_author FROM news WHERE news_caption LIKE('%') AND (news_id_author='1%')
Получили примерное представление о том, что представляет собой данная угроза. Начинаем продумывать архитуктуру приложения с учетом полученной информации, одновременно стараясь максимально упростить процесс работы с системой.
Один из вариантов исполнения задачи
В случае, если все параметры для составления БД будут связаны в единое целое из одиночных параметров, вероятность проведения любой атаки будет нецелесообразной, поскольку единственный меняющийся параметр WHERE будет динамически фильтроваться. Чтобы применить все возможности ООП, можем реализовать запросы вида:
$database = Database::getInstance();
$database->query()
->select( '*' )
->from( 'table' )
->where(
array(
'tName' => $_GET[ 'n' ],
'&tSurname' => $_GET[ 's' ]
)
)
->orderBy( 'tID', 'DESC' )
->limit( 10 )
->build();
Начинаем реализовывать приложение:
<?php
class Database {
private $queryInfo = array();
private $connection = null;
// TRUE для вывода запросов без обращений к БД; FALSE для подключения к БД и выполнения запросов
const DebugMode = true;
public function __construct( $server, $username, $password, $database ) {
if( self::DebugMode === true ) return true;
if( ! $this->connection = mysql_connect( $server, $username, $password ) ) trigger_error( 'Unable to connect to MySQL' );
if( ! mysql_select_db( $database, $this->connection ) ) trigger_error( 'Unable to connect to database' );
return true;
}
public function __call( $function, $args ) {
switch( strtolower( $function ) ) {
case 'select':
case 'update':
case 'delete':
case 'insert':
$this->queryInfo[ 'command' ] = strtolower( $function );
if( strtolower( $function ) == 'update' ) $this->queryInfo[ 'table' ] = strtolower( $args[ 0 ] );
break;
case 'select':
$this->queryInfo[ 'what' ] = strtolower( $args[ 0 ] );
break;
case 'from':
case 'to':
$this->queryInfo[ 'table' ] = strtolower( $args[ 0 ] );
break;
case 'limit':
$this->queryInfo[ 'limit' ] = (int) $args[ 0 ];
break;
case 'orderby':
$this->queryInfo[ 'orderBy' ] = $args[ 0 ];
$this->queryInfo[ 'orderMethod' ] = $args[ 1 ] == 'DESC' ? 'DESC' : 'ASC';
break;
case 'query':
$this->queryInfo = array();
break;
case 'where':
$where = null;
foreach( $args[ 0 ] as $tableName => $value ) {
$value = str_replace( '\'', '\\\'', $value );
$code = substr( $tableName, 0, 1 );
if( $code == '|' ) {
$tableName = substr( $tableName, 1 );
$where .= " OR $tableName='$value'";
} elseif( $code == '&' ) {
$tableName = substr( $tableName, 1 );
$where .= " AND $tableName='$value'";
} else {
$where .= " $tableName='$value'";
}
}
$this->queryInfo[ 'where' ] = $where;
break;
case 'set':
$setData = "";
foreach( $args[ 0 ] as $tableName => $value ) {
$value = str_replace( '\'', '\\\'', $value );
$setData .= " $tableName='$value',";
}
$this->queryInfo[ 'set' ] = substr( $setData, 0, strlen( $setData ) - 1 );
break;
case 'data':
$dataCols = "";
$dataValues = "";
foreach( $args[ 0 ] as $columnName => $value ) {
$value = str_replace( '\'', '\\\'', $value );
$dataCols .= "$columnName,";
$dataValues .= "'$value',";
}
$this->queryInfo[ 'data' ] = '(' . substr( $dataCols, 0, strlen( $dataCols ) - 1 ) . ') VALUES (' . substr( $dataValues, 0, strlen( $dataValues ) - 1 ) . ')';
break;
case 'build':
$builders = array(
'select' => "SELECT %what% FROM %table% %addit%",
'update' => "UPDATE %table% SET %set% %addit%",
'delete' => "DELETE FROM %table% %addit%",
'insert' => "INSERT INTO %table% %data%"
);
if( $this->queryInfo[ 'command' ] == null ) trigger_error( 'Command is not set' );
elseif( $this->queryInfo[ 'command' ] == 'select' ) {
if( $this->queryInfo[ 'table' ] == null ) trigger_error( 'Table to select data from is not set' );
$where = ( $this->queryInfo[ 'where' ] ? " WHERE {$this->queryInfo[ 'where' ]}" : null );
$limit = ( $this->queryInfo[ 'limit' ] ? " LIMIT {$this->queryInfo[ 'limit' ]}" : null );
$orderBy = ( $this->queryInfo[ 'orderBy' ] ? " ORDER BY {$this->queryInfo[ 'orderBy' ]} {$this->queryInfo[ 'orderMethod' ]}" : null );
$additional = "$where $orderBy $limit";
$replaces = array(
'%table%' => $this->queryInfo[ 'table' ],
'%what%' => ( $this->queryInfo[ 'what' ] ? $this->queryInfo[ 'what' ] : '*' ),
'%addit%' => $additional,
);
$this->sendQuery( strtr( $builders[ $this->queryInfo[ 'command' ] ], $replaces ) );
} elseif( $this->queryInfo[ 'command' ] == 'update' ) {
if( $this->queryInfo[ 'table' ] == null ) trigger_error( 'Table to update data is not set' );
if( $this->queryInfo[ 'set' ] == null ) trigger_error( 'Data to update is not set' );
$where = ( $this->queryInfo[ 'where' ] ? " WHERE {$this->queryInfo[ 'where' ]}" : null );
$additional = "$where";
$replaces = array(
'%table%' => $this->queryInfo[ 'table' ],
'%set%' => $this->queryInfo[ 'set' ],
'%addit%' => $additional,
);
$this->sendQuery( strtr( $builders[ $this->queryInfo[ 'command' ] ], $replaces ) );
} elseif( $this->queryInfo[ 'command' ] == 'insert' ) {
if( $this->queryInfo[ 'table' ] == null ) trigger_error( 'Table to update data is not set' );
if( $this->queryInfo[ 'data' ] == null ) trigger_error( 'Data to insert is not set' );
$replaces = array(
'%table%' => $this->queryInfo[ 'table' ],
'%data%' => $this->queryInfo[ 'data' ]
);
$this->sendQuery( strtr( $builders[ $this->queryInfo[ 'command' ] ], $replaces ) );
} elseif( $this->queryInfo[ 'command' ] == 'delete' ) {
if( $this->queryInfo[ 'table' ] == null ) trigger_error( 'Table to delete data from is not set' );
$where = ( $this->queryInfo[ 'where' ] ? " WHERE {$this->queryInfo[ 'where' ]}" : null );
$additional = "$where";
$replaces = array(
'%table%' => $this->queryInfo[ 'table' ],
'%addit%' => $additional
);
$this->sendQuery( strtr( $builders[ $this->queryInfo[ 'command' ] ], $replaces ) );
}
break;
}
return $this;
}
private function sendQuery( $query ) {
if( self::DebugMode === true ) print "$query<br />";
else return mysql_query( $query );
}
}
// Примеры команд
$database = new Database(
'127.0.0.1',
'root',
'test',
'test'
);
$database->query()
->select( '*' )
->from( 'table' )
->where(
array(
'tName' => $_GET[ 'n' ],
'&tSurname' => $_GET[ 's' ]
)
)
->orderBy( 'tID', 'DESC' )
->limit( 10 )
->build();
$database->query()
->update( 'table' )
->set(
array(
'tName' => '041',
'tSurname' => '854'
)
)
->where(
array(
'tName' => $_GET[ 'n' ],
'&tSurname' => $_GET[ 's' ]
)
)
->build();
$database->query()
->insert()
->to( 'table' )
->data(
array(
'tName' => '041',
'tSurname' => '854'
)
)
->build();
$database->query()
->delete()
->from( 'table' )
->where(
array(
'tName' => '041',
'|tSurname' => '854'
)
)
->build();
Мдааа, здец какой-то вышел, на самом деле. Не самый удачный пример. Надеюсь, у кого-нибудь есть еще идеи.