使用 PCRE 正则表达式验证 PHP 中的 SQL 时出现灾难性回溯

Catastrophic backtracking when using PCRE regexp to validate SQL in PHP

我正在尝试以一般形式验证一些 SQL:

UPDATE `mytable` SET `keyname` = 'keyvalue',
       `a` = 'somestring',
       `b` = 123,
       `c` = NULL
       WHERE `keyname` = 'keyvalue'

还有比这更多的字段。这些值将是字符串、整数或 NULL。

我原来的正则表达式是这样的:

(?ix)

^

\s*
UPDATE \s+ `mytable` \s+
SET \s+ `keyname` \s = \s 'keyvalue'
(, \s+
  `[A-Z_]+`  (?# field name)
  \s+ = \s+  (?# equals value)
  (
  -?[0-9]+         (?# an integer, possibly negative)
  |
  '(\.|''|[^'])*' (?# a string in single quotes)
  |
  NULL             (?# NULL)
  )
)+    (?# one or more such assignments)

\s+ WHERE \s+ `keyname` \s+ = \s+ 'keyvalue'

$

这在一定程度上有效。根据 https://regex101.com/ 它匹配 180 步。

可惜真正的SQL比那个长,例如:

UPDATE `mytable`
SET `keyname` = 'keyvalue',
`Markup` =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
''Quisque vel mattis odio, quis iaculis sem.''
Nulla facilisi.
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
cubilia Curae; Fusce ut dui venenatis, maximus lorem eget, ornare ex.
Aenean tempus pulvinar est, id fringilla enim sagittis id. Mauris finibus
cursus commodo.\r\n\r\n
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
cubilia Curae; Fusce ut dui venenatis, maximus lorem eget, ornare ex.
Aenean tempus pulvinar est, id fringilla enim sagittis id. Mauris finibus
cursus commodo.\r\n\r\n\r\n
\'Aenean in augue a est vulputate accumsan.\'
Phasellus nulla diam, laoreet a elit non, mattis finibus magna. Phasellus
faucibus iaculis mi sed pulvinar.\r\n
Aliquam non nisl ultricies, aliquam augue vitae, efficitur sapien.
Etiam viverra, magna a laoreet sollicitudin, ipsum erat tincidunt sem, nec
faucibus enim tortor eget massa.
Nunc nisi orci, lacinia vitae dictum et, vestibulum sed metus. ',
`From_Date` = NULL,
`To_Date` = NULL,
`Foo` = '',
`Box_Colour` = NULL,
`Modification_Date` = '2016-09-08 12:30:47',
`Modified_User` = 1,
`Modified_IP` = '192.168.1.1'
WHERE `keyname` = 'keyvalue'

现在需要 4301 步。事实上,如果您将 Lorem Ipsum 变大,我们将达到 20000 多步。

此外,如果我们引入错误(使其不匹配),例如更改:

`Foo` = '',

`Foo` = ''

它现在因灾难性回溯而崩溃。

我可以通过使内部组(key/value 对)成为原子组来摆脱灾难性回溯(在一定程度上)。即改变:

SET \s+ `keyname` \s = \s 'keyvalue'
(, \s+

SET \s+ `keyname` \s = \s 'keyvalue'
(?>, \s+

在我的目标 Web 服务器上 运行 时,真实数据的 20000 多步导致 PHP 脚本崩溃。我需要将步骤降低到一些更现实的价值。当正则表达式看起来相当明确时,我不太明白为什么会有这么多回溯。摆弄所有格量词或原子组似乎要么什么都不做,要么导致 "pass" 或 "fail" SQL 无法正确匹配。

编辑: 对于字符串子表达式:
使用@NikiC 的展开循环版本,
在需要的地方用 \s* 替换 \s
并添加一个额外的原子团,
它归结为合理的步骤。

https://regex101.com/r/yV5xI7/3

(编辑: 你也可以试试这个没有展开循环的版本 '(?>[^'\]+|\.|'')*' 相同的区别。 https://regex101.com/r/yV5xI7/5)

 (?si)

 ^ 

 \s* 
 UPDATE \s+ `mytable` \s+ 
 SET \s+ `keyname` \s* = \s* 'keyvalue'

 (?# one or more such key = value  )
 (?>
      \s* , \s* 

      (?# field name )
      ` [A-Z_]+ `  

      (?# equals )
      \s* = \s*  

      (?# value )
      (?>
           (?# an integer, possibly negative )
           -? [0-9]+         
        |  
           (?# or, a string )
           '
           [^'\]* 
           (?:
                (?: \ . | '' )
                [^'\]* 
           )*
           '

           # '
           # (?: [^'\] | '' | \ . )*
           # ' 

        |  
           (?# or, literal NULL )
           NULL             
      )
 )+

 \s+ WHERE \s+ `keyname` \s* = \s* 'keyvalue'

 $

您可以尝试将量化核心包裹在原子组中。
此外,对于点 . 的任何引用,您可能都需要点全修饰符。

并且,如果您在
之一中匹配它,则应该排除转义 字符串子表达式中的交替。
众所周知,引擎会采用匹配的路径。
在这种情况下,[^'] 也匹配转义,看起来像
转义不应被允许单独存在。
而且,实际上可能存在有效的转义 + 换行符序列,因此 (?s).
IE。使用 (?: \ . | '' | [^'\] )*

综上所述,这是ideone的

/
     (?si)
     ^ \s* UPDATE \s+ `mytable` \s+ SET \s+ `keyname` \s = \s 'keyvalue'
     (?>
          , \s+ ` [A-Z_]+ `
          (?# field name )
          \s+ = \s+ 
          (?# equals value )
          (?:
               -? [0-9]+ 
               (?# an integer, possibly negative )
            |  '
               (?: \ . | '' | [^'\] )*
               '
               (?# a string in single quotes )
            |  NULL
               (?# NULL )
          )
     )+
     (?# one or more such assignments )
     \s+ WHERE \s+ `keyname` \s+ = \s+ 'keyvalue' $
/x

还有一个 php

'/
     (?si)
     ^ \s* UPDATE \s+ `mytable` \s+ SET \s+ `keyname` \s = \s \'keyvalue\'
     (?>
          , \s+ ` [A-Z_]+ `
          (?# field name )
          \s+ = \s+ 
          (?# equals value )
          (?:
               -? [0-9]+ 
               (?# an integer, possibly negative )
            |  \'
               (?: \\ . | \'\' | [^\'\\] )*
               \'
               (?# a string in single quotes )
            |  NULL
               (?# NULL )
          )
     )+
     (?# one or more such assignments )
     \s+ WHERE \s+ `keyname` \s+ = \s+ \'keyvalue\' $
/x'