squizlabs / PHP_CodeSniffer

PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.
BSD 3-Clause "New" or "Revised" License
10.66k stars 1.48k forks source link

Infinite loop analyzing a certain file. #3942

Closed npr-gh closed 3 months ago

npr-gh commented 7 months ago

Describe the bug

PHPSniffer managed to run forever analyzing a certain CSS file. (I discovered later, 'practically forever', as in longer than the heat death of the universe, but still it theoretically would complete in a finite time).

After the verbose mode created a 17-gigabyte log, I cut it off.

The section of the CSS file that causes the problem is this bit of code.

#available-widgets .widget-title:before {
        content: "\f132";
        position: absolute;
        top: -3px;
        left: 100%;
        margin-left: 20px;
        width: 20px;
        height: 20px;
        color: #2c3338;
        font: normal 20px/1 dashicons;
        text-align: center;
        box-sizing: border-box;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
}

/* dashicons-smiley */
#available-widgets [class*="easy"] .widget-title:before { content: "\f328"; top: -4px; }

/* dashicons-star-filled */
#available-widgets [class*="super"] .widget-title:before,
#available-widgets [class*="like"] .widget-title:before { content: "\f155"; top: -4px; }

/* dashicons-wordpress */
#available-widgets [class*="meta"] .widget-title:before { content: "\f120"; }

/* dashicons-archive */
#available-widgets [class*="archives"] .widget-title:before { content: "\f480"; top: -4px; }

/* dashicons-category */
#available-widgets [class*="categor"] .widget-title:before { content: "\f318"; top: -4px; }

/* dashicons-admin-comments */
#available-widgets [class*="comment"] .widget-title:before,
#available-widgets [class*="testimonial"] .widget-title:before,
#available-widgets [class*="chat"] .widget-title:before { content: "\f101"; }

/* dashicons-admin-post */
#available-widgets [class*="post"] .widget-title:before { content: "\f109"; }

/* dashicons-admin-page */
#available-widgets [class*="page"] .widget-title:before { content: "\f105"; }

/* dashicons-text */
#available-widgets [class*="text"] .widget-title:before { content: "\f478"; }

/* dashicons-admin-links */
#available-widgets [class*="link"] .widget-title:before { content: "\f103"; }

/* dashicons-search */
#available-widgets [class*="search"] .widget-title:before { content: "\f179"; }

/* dashicons-menu */
#available-widgets [class*="menu"] .widget-title:before,
#available-widgets [class*="nav"] .widget-title:before { content: "\f333"; }

/* dashicons-tagcloud */
#available-widgets [class*="tag"] .widget-title:before { content: "\f479"; }

/* dashicons-rss */
#available-widgets [class*="rss"] .widget-title:before { content: "\f303"; top: -6px; }

/* dashicons-calendar */
#available-widgets [class*="event"] .widget-title:before,
#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;}

/* dashicons-format-image */
#available-widgets [class*="image"] .widget-title:before,
#available-widgets [class*="photo"] .widget-title:before,
#available-widgets [class*="slide"] .widget-title:before,
#available-widgets [class*="instagram"] .widget-title:before { content: "\f128"; }

/* dashicons-format-gallery */
#available-widgets [class*="album"] .widget-title:before,
#available-widgets [class*="galler"] .widget-title:before { content: "\f161"; }

/* dashicons-format-video */
#available-widgets [class*="video"] .widget-title:before,
#available-widgets [class*="tube"] .widget-title:before { content: "\f126"; }

/* dashicons-format-audio */
#available-widgets [class*="music"] .widget-title:before,
#available-widgets [class*="radio"] .widget-title:before,
#available-widgets [class*="audio"] .widget-title:before { content: "\f127"; }

/* dashicons-admin-users */
#available-widgets [class*="login"] .widget-title:before,
#available-widgets [class*="user"] .widget-title:before,
#available-widgets [class*="member"] .widget-title:before,
#available-widgets [class*="avatar"] .widget-title:before,
#available-widgets [class*="subscriber"] .widget-title:before,
#available-widgets [class*="profile"] .widget-title:before,
#available-widgets [class*="grofile"] .widget-title:before { content: "\f110"; }

/* dashicons-cart */
#available-widgets [class*="commerce"] .widget-title:before,
#available-widgets [class*="shop"] .widget-title:before,
#available-widgets [class*="cart"] .widget-title:before { content: "\f174"; top: -4px; }

/* dashicons-shield */
#available-widgets [class*="secur"] .widget-title:before,
#available-widgets [class*="firewall"] .widget-title:before { content: "\f332"; }

/* dashicons-chart-bar */
#available-widgets [class*="analytic"] .widget-title:before,
#available-widgets [class*="stat"] .widget-title:before,
#available-widgets [class*="poll"] .widget-title:before { content: "\f185"; }

/* dashicons-feedback */
#available-widgets [class*="form"] .widget-title:before { content: "\f175"; }

/* dashicons-email-alt */
#available-widgets [class*="subscribe"] .widget-title:before,
#available-widgets [class*="news"] .widget-title:before,
#available-widgets [class*="contact"] .widget-title:before,
#available-widgets [class*="mail"] .widget-title:before { content: "\f466"; }

/* dashicons-share */
#available-widgets [class*="share"] .widget-title:before,
#available-widgets [class*="socia"] .widget-title:before { content: "\f237"; }

/* dashicons-translation */
#available-widgets [class*="lang"] .widget-title:before,
#available-widgets [class*="translat"] .widget-title:before { content: "\f326"; }

/* dashicons-location-alt */
#available-widgets [class*="locat"] .widget-title:before,
#available-widgets [class*="map"] .widget-title:before { content: "\f231"; }

/* dashicons-download */
#available-widgets [class*="download"] .widget-title:before { content: "\f316"; }

/* dashicons-cloud */
#available-widgets [class*="weather"] .widget-title:before { content: "\f176"; top: -4px;}

/* dashicons-facebook */
#available-widgets [class*="facebook"] .widget-title:before { content: "\f304"; }

/* dashicons-twitter */
#available-widgets [class*="tweet"] .widget-title:before,
#available-widgets [class*="twitter"] .widget-title:before { content: "\f301"; }

The issue is a very gross inefficiency on the part of PHPSniffer, it seems to scale as O(2^n).

For each line of "#available_widgets", it appears that the entire file is scanned again, recursively. This creates an exponential amount of work.

To reproduce

Run the following on the command line:

php /usr/src/phpSniffer/phpcs.phar -p test.css --standard=PHPCompatibilityWP -vvv | tee test.txt

Expected behaviour

Actual behaviour:

A little interesting experiment that really exposes the behaviour is to take the initial block of CSS only, then add the #available-widgets lines block by block. I started at a random position in the middle /* dashicons-admin-users */, and added blocks one by one, then looked at the execution time. I didn't do a lot of sampling so there's quite a bit of statistical error here. The exponential behaviour is pretty clear after a few iterations of adding more lines to the file.

+-------+-------+-------+
| lines | done  | time  |
+-------+-------+-------+
|     7 | 316ms | 425ms |
|    10 | 710ms | 820ms |
|    15 | 1.9s  | 2.0s  |
|    16 | 2.44s | 2.65s |
|    20 | 17s   | 17.2s |
|    22 | 36.5s | 36.7s |
|    24 | 71.2s | 72.0s |
+-------+-------+-------+

t ~ 23.383e(0.325L) where t = time in ms, L = number of '#available-widgets' lines is a least-squares exponential fit to this data.

Versions (please complete the following information)

Operating System: Debian 12.
PHP version 8.3
PHP_CodeSniffer version master
Standard PHPCompatibilityWP
Install type PHAR / Git clone.

Additional context

This can cause the viability of Denial-of-service attacks if PHPCodeSniffer / PHPCompatibilityWP is provided as a service and may be a security issue in this context.

Please confirm:

martinjoiner commented 6 months ago

Based on the information you provide this seems like a very contrived example. You suggest it is a potential vector for a denial-of-service attack but only in the situation where:

  1. Someone is providing PHPCodeSniffer as a service in production
  2. There are no limits on the request to protect
  3. They are allowing CSS to be fed into a static analysis tool for PHP

This seems like a totally unrealistic example and nowhere near the normal use case for PHPCodeSniffer. Why are you feeding CSS into a tool for PHP?

jrfnl commented 6 months ago

@npr-gh @martinjoiner Well, PHPCS does currently still handle CSS and JS (up to a point), but the PHPCompatibility rulesets never have, nor will.

As per the readme on those projects: use --extensions=php when running the PHPCompatibility rulesets.

Or if combined with other rulesets, use the below in a custom ruleset to limit PHPCompatibility to PHP files:

    <rule ref="PHPCompatibilityWP">
        <include-pattern>*\.php$</include-pattern>
    </rule>

(though the problem is likely to be in the CSS tokenizer, but as that will be removed in v4, this is unlikely to be fixed)

On another note: don't open issues in this repo anymore, use the new repo instead. See #3932.