RegExp: new "Exponential backtracking" inspection

This commit is contained in:
Bas Leijdekkers
2017-02-07 12:43:54 +01:00
parent 051acded35
commit c5f4701f6c
6 changed files with 180 additions and 1 deletions
@@ -34,5 +34,7 @@
implementationClass="org.intellij.lang.regexp.inspection.SingleCharAlternationInspection"/>
<localInspection groupName="RegExp" language="RegExp" shortName="OctalEscape" displayName="Octal escape" enabledByDefault="false"
level="WARNING" implementationClass="org.intellij.lang.regexp.inspection.OctalEscapeInspection"/>
<localInspection groupName="RegExp" language="RegExp" shortName="ReDoS" displayName="Exponential backtracking" enabledByDefault="true"
level="WARNING" implementationClass="org.intellij.lang.regexp.inspection.ReDoSInspection"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,14 @@
<html>
<body>
Reports exponential backtracking in a RegExp, which can lead to extremely slow matching.
Exponential backtracking can happen when a single string can be matched in multiple ways, leading to exponential runtime.
Exponential backtracking is also known as Regular Expression Denial of Service (ReDoS), catastrophic backtracking, explosive quantifiers
and exponential matching.
This inspection only detects a limited form of exponential backtracking,
so the absence of a warning is not a guarantee the pattern does not contain a exponential backtracking problem.
<!-- tooltip end -->
<p>
<small>New in 2017.1</small>
</body>
</html>
@@ -0,0 +1,105 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.lang.regexp.inspection;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import org.intellij.lang.regexp.RegExpTT;
import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class ReDoSInspection extends LocalInspectionTool {
@Override
public void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder problemsHolder) {
super.inspectionFinished(session, problemsHolder);
}
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
return new ReDoSVisitor(holder);
}
private static class ReDoSVisitor extends RegExpElementVisitor {
private final ProblemsHolder myHolder;
public ReDoSVisitor(ProblemsHolder holder) {
myHolder = holder;
}
@Override
public void visitRegExpClosure(RegExpClosure closure) {
if (!hasSuspiciousQuantifier(closure)) {
return;
}
RegExpClosure parent = PsiTreeUtil.getParentOfType(closure, RegExpClosure.class);
while (parent != null) {
if (hasSuspiciousQuantifier(parent)) {
if (isAtomic(closure)) {
return;
}
myHolder.registerProblem(parent, "Potential exponential backtracking");
return;
}
parent = PsiTreeUtil.getParentOfType(parent, RegExpClosure.class);
}
}
private static boolean hasSuspiciousQuantifier(RegExpClosure closure) {
final RegExpQuantifier quantifier = closure.getQuantifier();
if (!quantifier.isCounted()) {
final ASTNode token = quantifier.getToken();
return !(token == null || token.getElementType() == RegExpTT.QUEST);
}
final RegExpNumber max = quantifier.getMax();
if (max == null) {
return true;
}
final Number value = max.getValue();
return value == null || value.doubleValue() >= 10;
}
private static boolean isAtomic(RegExpAtom element) {
while (element != null) {
if (element instanceof RegExpClosure) {
final RegExpClosure closure = (RegExpClosure)element;
if (closure.getQuantifier().isPossessive()) {
return true;
}
}
else if (element instanceof RegExpGroup) {
final RegExpGroup group = (RegExpGroup)element;
if (group.getType() == RegExpGroup.Type.ATOMIC) {
return true;
}
}
element = PsiTreeUtil.getParentOfType(element, RegExpClosure.class, RegExpGroup.class);
}
return false;
}
}
}
@@ -15,11 +15,14 @@
*/
package org.intellij.lang.regexp.psi;
import org.jetbrains.annotations.Nullable;
/**
* Number consisting only of one or more decimal digits. No negation or decimal points.
* @author Bas Leijdekkers
*/
public interface RegExpNumber extends RegExpElement {
@Nullable
Number getValue();
}
@@ -16,8 +16,10 @@
package org.intellij.lang.regexp.psi.impl;
import com.intellij.lang.ASTNode;
import org.intellij.lang.regexp.RegExpLanguageHosts;
import org.intellij.lang.regexp.psi.RegExpElementVisitor;
import org.intellij.lang.regexp.psi.RegExpNumber;
import org.jetbrains.annotations.Nullable;
/**
* @author Bas Leijdekkers
@@ -28,6 +30,12 @@ public class RegExpNumberImpl extends RegExpElementImpl implements RegExpNumber
super(node);
}
@Nullable
@Override
public Number getValue() {
return RegExpLanguageHosts.getInstance().getQuantifierValue(this);
}
@Override
public void accept(RegExpElementVisitor visitor) {
visitor.visitRegExpNumber(this);
@@ -0,0 +1,47 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.lang.regexp.inspection;
import com.intellij.codeInspection.LocalInspectionTool;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class ReDoSInspectionTest extends RegExpInspectionTestCase {
public void testNestedQuantifiers1() {
highlightTest("<warning descr=\"Potential exponential backtracking\">(.*a){10}</warning>");
}
public void testNestedQuantifiers2() {
highlightTest("<warning descr=\"Potential exponential backtracking\">(a+)+</warning>");
}
public void testPossessiveQuantifier() {
highlightTest("(a+)++");
}
public void testAtomicGroup() {
highlightTest("(?>a+)+");
}
@NotNull
@Override
protected LocalInspectionTool getInspection() {
return new ReDoSInspection();
}
}