├── settings.gradle ├── docs ├── doctrine │ ├── doctrine_sample.png │ ├── doctrine_solution.png │ └── native_query_sample.png └── mysqli │ ├── mysqli_samples_object.png │ ├── mysqli_samples_functional.png │ └── mysqli_samples_solution.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src └── main │ ├── java │ └── com │ │ └── theodo │ │ └── php │ │ └── plugins │ │ └── injection │ │ ├── ConstantHelper.java │ │ ├── ParameterTypeHelper.java │ │ ├── ParameterHelper.java │ │ └── SQLInjectionHighlighting.java │ └── resources │ └── META-INF │ └── plugin.xml ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sqlinjection-php-ide-plugin' 2 | 3 | -------------------------------------------------------------------------------- /docs/doctrine/doctrine_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/doctrine/doctrine_sample.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/doctrine/doctrine_solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/doctrine/doctrine_solution.png -------------------------------------------------------------------------------- /docs/doctrine/native_query_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/doctrine/native_query_sample.png -------------------------------------------------------------------------------- /docs/mysqli/mysqli_samples_object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/mysqli/mysqli_samples_object.png -------------------------------------------------------------------------------- /docs/mysqli/mysqli_samples_functional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/mysqli/mysqli_samples_functional.png -------------------------------------------------------------------------------- /docs/mysqli/mysqli_samples_solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo/php-sql-injection-detection-ide-plugin/HEAD/docs/mysqli/mysqli_samples_solution.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 13 13:58:07 CEST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # env 10 | .env 11 | 12 | # compiled files 13 | /dist 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | package-lock.json 23 | 24 | # IDE 25 | .idea 26 | .gradle 27 | -------------------------------------------------------------------------------- /src/main/java/com/theodo/php/plugins/injection/ConstantHelper.java: -------------------------------------------------------------------------------- 1 | package com.theodo.php.plugins.injection; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.jetbrains.php.lang.psi.elements.*; 5 | import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; 6 | 7 | import static com.theodo.php.plugins.injection.ParameterHelper.checkElement; 8 | 9 | class ConstantHelper { 10 | static boolean isConstant(PsiElement operand) { 11 | ConstantSourceDetection visitor = new ConstantSourceDetection(); 12 | visitor.apply(operand); 13 | return visitor.isConstant; 14 | } 15 | 16 | private static class ConstantSourceDetection extends PhpElementVisitor { 17 | private boolean isConstant = false; 18 | 19 | 20 | @Override 21 | public void visitPhpStringLiteralExpression(StringLiteralExpression expression) { 22 | if( expression.isSingleQuote()) { 23 | isConstant = true; 24 | } else { 25 | PsiElement[] children = expression.getChildren(); 26 | for (PsiElement child : children) { 27 | if(!isConstant(child)) { 28 | return; 29 | } 30 | } 31 | isConstant = true; 32 | } 33 | } 34 | 35 | @Override 36 | public void visitPhpClassConstantReference(ClassConstantReference constantReference) { 37 | isConstant = true; 38 | } 39 | 40 | @Override 41 | public void visitPhpArrayAccessExpression(ArrayAccessExpression expression) { 42 | PhpPsiElement value = expression.getValue(); 43 | isConstant = isConstant(value); 44 | } 45 | 46 | @Override 47 | public void visitPhpVariable(Variable variable) { 48 | isConstant = !checkElement(variable); 49 | } 50 | 51 | @Override 52 | public void visitPhpBinaryExpression(BinaryExpression expression) { 53 | if (expression instanceof ConcatenationExpression) { 54 | ConcatenationExpression concatenationExpression = (ConcatenationExpression) expression; 55 | isConstant = isConstant(concatenationExpression.getLeftOperand()) && 56 | isConstant(concatenationExpression.getRightOperand()); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | org.example.sqlinjection-php-ide-plugin 3 | Potential SqlInjection detection for PHP frameworks 4 | 0.5 5 | Theodo 6 | 7 | 9 | See: https://en.wikipedia.org/wiki/SQL_injection
10 | ]]>
11 | 12 | 24 | 25 | 26 | 27 | com.intellij.modules.platform 28 | com.jetbrains.php 29 | 30 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 50 |
-------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/main/java/com/theodo/php/plugins/injection/ParameterTypeHelper.java: -------------------------------------------------------------------------------- 1 | package com.theodo.php.plugins.injection; 2 | 3 | import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment; 4 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocParamTag; 5 | import com.jetbrains.php.lang.psi.elements.Function; 6 | import com.jetbrains.php.lang.psi.elements.Method; 7 | import com.jetbrains.php.lang.psi.elements.Parameter; 8 | import com.jetbrains.php.lang.psi.elements.PhpClass; 9 | import com.jetbrains.php.lang.psi.resolve.types.PhpType; 10 | 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | class ParameterTypeHelper { 16 | 17 | private final Map typePerParameter = new HashMap<>(); 18 | private final Map namePerParameter = new HashMap<>(); 19 | 20 | ParameterTypeHelper(Function function) { 21 | if(function != null) { 22 | getParametersType(function); 23 | } 24 | } 25 | 26 | private void getParametersType(Function function) { 27 | 28 | function = findMethodDefinitionInInterface(function); 29 | 30 | Parameter[] parameters = function.getParameters(); 31 | getParameterTypesFromMethod(parameters); 32 | getParameterTypesFromPhpDoc(function); 33 | } 34 | 35 | private void getParameterTypesFromPhpDoc(Function function) { 36 | if (typePerParameter.isEmpty()) { 37 | PhpDocComment docComment = function.getDocComment(); 38 | if (docComment != null) { 39 | List paramTags = docComment.getParamTags(); 40 | int index = 0; 41 | for (PhpDocParamTag paramTag : paramTags) { 42 | namePerParameter.put(index, paramTag.getVarName()); 43 | typePerParameter.put(index++, paramTag.getType()); 44 | } 45 | } 46 | } 47 | } 48 | 49 | private void getParameterTypesFromMethod(Parameter[] parameters) { 50 | int index = 0; 51 | for (Parameter parameter : parameters) { 52 | namePerParameter.put(index, parameter.getName()); 53 | typePerParameter.put(index++, parameter.getType()); 54 | } 55 | } 56 | 57 | private Function findMethodDefinitionInInterface(Function function) { 58 | if(function instanceof Method){ 59 | Method method = (Method) function; 60 | PhpClass containingClass = method.getContainingClass(); 61 | if(containingClass == null) return function; 62 | 63 | PhpClass[] implementedInterfaces = containingClass.getImplementedInterfaces(); 64 | for (PhpClass implementedInterface : implementedInterfaces) { 65 | Method methodByName = implementedInterface.findMethodByName(function.getName()); 66 | if(methodByName != null) return methodByName; 67 | } 68 | } 69 | return function; 70 | } 71 | 72 | PhpType getType(int paramIndex){ 73 | PhpType phpType = typePerParameter.get(paramIndex); 74 | if(phpType == null) return PhpType.MIXED; 75 | return phpType; 76 | } 77 | 78 | String getName(int paramIndex){ 79 | String name = namePerParameter.get(paramIndex); 80 | if(name == null) return "???"; 81 | return name; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/theodo/php/plugins/injection/ParameterHelper.java: -------------------------------------------------------------------------------- 1 | package com.theodo.php.plugins.injection; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.intellij.psi.PsiReference; 5 | import com.intellij.psi.search.searches.ReferencesSearch; 6 | import com.intellij.util.Query; 7 | import com.jetbrains.php.lang.psi.elements.*; 8 | import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; 9 | 10 | import static com.theodo.php.plugins.injection.ConstantHelper.isConstant; 11 | 12 | class ParameterHelper { 13 | static boolean checkElement(PsiElement psiElement) { 14 | if(psiElement == null) return false; 15 | 16 | DetectIssuesVisitor visitor = new DetectIssuesVisitor(); 17 | visitor.apply(psiElement); 18 | return visitor.hasIssue; 19 | } 20 | 21 | 22 | private static class DetectIssuesVisitor extends PhpElementVisitor { 23 | private boolean hasIssue = false; 24 | 25 | @Override 26 | public void visitPhpBinaryExpression(BinaryExpression expression) { 27 | hasIssue = !isConstant(expression); 28 | } 29 | 30 | 31 | @Override 32 | public void visitPhpVariable(Variable variable) { 33 | if(!variable.isDeclaration() && variable.resolve() != null){ 34 | // Jump to Declaration and test it 35 | hasIssue = checkElement(variable.resolve()); 36 | } else { 37 | // All variable assignments must come from constant sources otherwise it's dangerous 38 | SearchNonConstAssignments searchNonConstAssignments = new SearchNonConstAssignments(); 39 | if (searchNonConstAssignments.check(variable)) { 40 | hasIssue = true; 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public void visitPhpField(Field field) { 47 | hasIssue = true; 48 | } 49 | 50 | @Override 51 | public void visitPhpParameter(Parameter parameter) { 52 | hasIssue = true; 53 | } 54 | 55 | @Override 56 | public void visitPhpFunctionCall(FunctionReference reference) { 57 | hasIssue = true; 58 | } 59 | 60 | @Override 61 | public void visitPhpStringLiteralExpression(StringLiteralExpression expression) { 62 | if(expression.isSingleQuote()) return; 63 | 64 | // CHECK for variables in String Interpolation 65 | PsiElement[] children = expression.getChildren(); 66 | for (PsiElement child : children) { 67 | if(checkElement(child)){ 68 | hasIssue = true; 69 | return; 70 | } 71 | } 72 | } 73 | } 74 | 75 | private static class SearchNonConstAssignments { 76 | private boolean atLeastOneNonConstant = false; 77 | 78 | private boolean check(Variable variable) { 79 | atLeastOneNonConstant = false; 80 | Query search = ReferencesSearch.search(variable); 81 | search.findAll().forEach(psiReference -> { 82 | if (psiReference instanceof Variable && ((Variable) psiReference).isDeclaration()) { 83 | PsiElement parent = ((Variable) psiReference).getParent(); 84 | if (parent instanceof AssignmentExpression) { 85 | AssignmentExpression assignmentExpression = (AssignmentExpression) parent; 86 | PhpPsiElement value = assignmentExpression.getValue(); 87 | boolean constant = isConstant(value); 88 | if (!constant) { 89 | atLeastOneNonConstant = true; 90 | } 91 | } 92 | } 93 | }); 94 | return atLeastOneNonConstant; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SQL Injection Detection Plugin for PHPStorm IDE 2 | Author: Guillaume MICHEL (guillaumem@theodo.fr) 3 | 4 | ## MySQL Improved Extension 5 | See: https://www.php.net/manual/en/book.mysqli.php 6 | #### mysqli Code samples: 7 | ###### no Problem highlighted: 8 | ``` php 9 | public function noProblemFound($link) 10 | { 11 | // constant string 12 | mysqli_query($link, "SELECT * FROM Data WHERE aColumn='aValue'"); 13 | 14 | // variable with only constant assignments 15 | $sql = "SELECT * FROM Data WHERE aColumn='aValue'"; 16 | mysqli_query($link, $sql); 17 | 18 | // constant concatenation of constants 19 | mysqli_query($link, "SELECT * FROM Data WHERE " . " aColumn='aValue'"); 20 | 21 | // constant concatenation of constants 22 | mysqli_query($link, "SELECT * FROM Data WHERE " . MyClass::aConstant); 23 | } 24 | ``` 25 | 26 | ###### Potential SQL Injection warning issued: 27 | ``` php 28 | public function warningFound($link, $param) 29 | { 30 | // concatenation that implies class field (non constant) 31 | // If $this->aField is not constant, its value is not controlled. It can be used to inject malicious condition. 32 | mysqli_query($link, "SELECT * FROM Data WHERE aColumn=" . $this->aField); 33 | // Prefer: 34 | // $stmt = $mysqli->prepare("SELECT * FROM Data WHERE aColumn=?"); 35 | // $stmt->bind_param("s", $this->aField); 36 | // See: https://www.php.net/manual/en/mysqli.prepare.php 37 | 38 | // concatenation that implies method parameter (non constant) 39 | // If $param is not constant, its value is not controlled. It can be used to inject malicious condition. 40 | mysqli_query($link, "SELECT * FROM Data WHERE aColumn=" . $param); 41 | 42 | // concatenation that implies non constant variable(non constant) 43 | $sql = $this->getValue(); 44 | mysqli_query($link, "SELECT * FROM Data WHERE aColumn=" . $sql); 45 | } 46 | ``` 47 | 48 | ![alt text](./docs/mysqli/mysqli_samples_functional.png "Samples for SQLi Functional calls") 49 | 50 | ![alt text](./docs/mysqli/mysqli_samples_object.png "Samples for SQLi Object calls") 51 | 52 | ![alt text](./docs/mysqli/mysqli_samples_solution.png "Use Parameterized Queries") 53 | 54 | ## Doctrine 55 | See: https://www.doctrine-project.org/ 56 | #### Doctrine EntityManager Code samples: 57 | ###### Potential SQL Injection warning issued: 58 | ``` php 59 | public function warningFound($entityManager, $rsm, $param) 60 | { 61 | // concatenation that implies method parameter (non constant) 62 | $query = $entityManager->createNativeQuery('SELECT * FROM users WHERE name = ' . $param, $rsm); 63 | 64 | // concatenation that implies non constant variable (non constant) 65 | $sql = $this->getValue(); 66 | $query = $entityManager->createNativeQuery('SELECT * FROM users WHERE name = ' . $sql, $rsm); 67 | 68 | // concatenation that implies class field (non constant) 69 | $query = $entityManager->createNativeQuery('SELECT * FROM users WHERE name = ' . $this->aField, $rsm); 70 | 71 | // and so on... 72 | } 73 | ``` 74 | 75 | ![alt text](./docs/doctrine/native_query_sample.png "Samples for Native Queries") 76 | 77 | 78 | #### Doctrine QueryBuilder Code samples: 79 | ###### no Problem highlighted: 80 | ``` php 81 | public function noProblemFound() 82 | { 83 | // constant concatenation 84 | $this->createQueryBuilder('u') 85 | ->andWhere('u.reference LIKE ' . 'ggggg') 86 | ->getQuery() 87 | ->getOneOrNullResult(); 88 | 89 | // constant concatenation 90 | $this->createQueryBuilder('u') 91 | ->andWhere('u.reference LIKE ' . MyClass::aConstant) 92 | ->getQuery() 93 | ->getOneOrNullResult(); 94 | 95 | // constant concatenation 96 | $this->createQueryBuilder('u') 97 | ->andWhere('u.reference LIKE ' . MyClass::constantsArray[0]) 98 | ->getQuery() 99 | ->getOneOrNullResult(); 100 | 101 | // variable with only constant assignments 102 | $name = 'ttt'; 103 | $this->createQueryBuilder('u') 104 | ->andWhere('u.reference LIKE ' . $name) 105 | ->getQuery() 106 | ->getOneOrNullResult(); 107 | } 108 | ``` 109 | 110 | ###### Potential SQL Injection warning issued: 111 | ``` php 112 | public function warningFound($param) 113 | { 114 | // concatenation that implies method parameter (non constant) 115 | $this->createQueryBuilder('u') 116 | ->andWhere('u.reference LIKE ' . $param) 117 | ->getQuery() 118 | ->getOneOrNullResult(); 119 | 120 | // concatenation that implies method field (non constant) 121 | $this->createQueryBuilder('u') 122 | ->andWhere('u.reference LIKE ' . $this->aField) 123 | ->getQuery() 124 | ->getOneOrNullResult(); 125 | 126 | // direct usage of param 127 | $this->createQueryBuilder('u') 128 | ->andWhere($param) // RISKY 129 | ->getQuery() 130 | ->getOneOrNullResult(); 131 | 132 | // direct usage of field 133 | $this->createQueryBuilder('u') 134 | ->andWhere($this->aField) // RISKY 135 | ->getQuery() 136 | ->getOneOrNullResult(); 137 | 138 | // variable with at least one non constant assignments 139 | $name = 'ttt'; 140 | $name = $this->getValue(); 141 | $this->createQueryBuilder('u') 142 | ->andWhere('u.reference LIKE ' . $name) 143 | ->getQuery() 144 | ->getOneOrNullResult(); 145 | } 146 | ``` 147 | ![alt text](./docs/doctrine/doctrine_sample.png "Samples for QueryBuilder") 148 | 149 | ![alt text](./docs/doctrine/doctrine_solution.png "Use Parameterized Queries") 150 | 151 | 152 | ### Changes: 153 | * First version of the plugin. 154 | * Manage more precisely function arguments 155 | * Manage variables flow (detect non constant assignments) 156 | * When parameter value of SQL methods is constant or constant composition: no problem 157 | * When parameter value of SQL methods is a parameter of enclosing function: warning issued 158 | * When parameter value of SQL methods is class field (non constant): warning issued 159 | * When parameter value of SQL methods is a local variable, check if all assignments are constant 160 | * For local variables, when at least one assignment is not constant: issues a warning 161 | * Focus only on parameters of type String or Mixed (unknown) 162 | * Type detection retrieved from both php code (class + interfaces) and from php-doc 163 | -------------------------------------------------------------------------------- /src/main/java/com/theodo/php/plugins/injection/SQLInjectionHighlighting.java: -------------------------------------------------------------------------------- 1 | package com.theodo.php.plugins.injection; 2 | 3 | import com.intellij.codeInspection.ProblemsHolder; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiElementVisitor; 7 | import com.jetbrains.php.lang.inspections.PhpInspection; 8 | import com.jetbrains.php.lang.psi.elements.*; 9 | import com.jetbrains.php.lang.psi.resolve.types.PhpType; 10 | import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | 14 | public class SQLInjectionHighlighting extends PhpInspection { 15 | @NotNull 16 | @Override 17 | public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder problemsHolder, boolean onTheFly) { 18 | 19 | return new PhpElementVisitor() { 20 | public void visitPhpMethodReference(MethodReference reference) { 21 | super.visitPhpMethodReference(reference); 22 | PhpExpression classReference = reference.getClassReference(); 23 | 24 | if (classReference != null) { 25 | Project project = reference.getProject(); 26 | PhpType type = classReference.getType().global(project); 27 | if (type.toString().contains("\\Doctrine\\ORM\\QueryBuilder")) { 28 | if(reference.getName() != null && reference.getName().toUpperCase().contains("WHERE")) { 29 | inspectAllParameters(reference, problemsHolder, "Doctrine QueryBuilder: "); 30 | } 31 | } 32 | if (type.toString().contains("\\Doctrine\\ORM\\EntityManager")) { 33 | if(reference.getName() != null && reference.getName().toUpperCase().contains("NATIVEQUERY")) { 34 | inspectAllParameters(reference, problemsHolder, "EntityManager NativeQuery: "); 35 | } 36 | } 37 | if (type.toString().contains("\\mysqli") && reference.getName() != null) { 38 | if (reference.getName().contains("query") || reference.getName().contains("prepare")) { 39 | inspectAllParameters(reference, problemsHolder, "mysqli: "); 40 | } 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public void visitPhpFunctionCall(FunctionReference reference) { 47 | if (reference.getName() != null && reference.getName().contains("mysqli_")) { 48 | if (reference.getName().contains("query") || reference.getName().contains("prepare")) { 49 | inspectAllParameters(reference, problemsHolder, "mysqli: "); 50 | } 51 | } 52 | if (reference.getName() != null && reference.getName().contains("pg_")) { 53 | if (reference.getName().contains("_query")) { 54 | inspectAllParameters(reference, problemsHolder, "pg: "); 55 | } 56 | } 57 | } 58 | 59 | }; 60 | } 61 | 62 | private void inspectAllParameters(FunctionReference functionReference, 63 | @NotNull ProblemsHolder problemsHolder, 64 | String why) { 65 | Function function = (Function) functionReference.resolve(); 66 | if (function == null) return; 67 | 68 | ParameterList parameterList = functionReference.getParameterList(); 69 | if (parameterList == null) { 70 | return; 71 | } 72 | 73 | PsiElement[] parameters = parameterList.getParameters(); 74 | ParameterTypeHelper parameterTypeHelper = new ParameterTypeHelper(function); 75 | int index = 0; 76 | for (PsiElement parameter : parameters) { 77 | PhpType type = parameterTypeHelper.getType(index); 78 | if (PhpType.STRING.equals(type) || PhpType.MIXED.equals(type)) { 79 | String paramName = parameterTypeHelper.getName(index); 80 | PhpElementVisitor visitor = new ParameterVisitor(why, function, parameter, paramName, problemsHolder); 81 | visitor.apply(parameter); 82 | } 83 | index++; 84 | } 85 | 86 | } 87 | 88 | private final static class ParameterVisitor extends PhpElementVisitor { 89 | private final Function function; 90 | private final ProblemsHolder problemsHolder; 91 | private final PsiElement argument; 92 | private final String why; 93 | private final String paramName; 94 | 95 | ParameterVisitor(String why, Function function, PsiElement argument, String paramName, ProblemsHolder problemsHolder) { 96 | this.function = function; 97 | this.problemsHolder = problemsHolder; 98 | this.argument = argument; 99 | this.why = why; 100 | this.paramName = paramName; 101 | } 102 | 103 | @Override 104 | public void visitPhpBinaryExpression(BinaryExpression expression) { 105 | if (ParameterHelper.checkElement(expression)) { 106 | problemsHolder.registerProblem(argument, 107 | why + "String Concatenation found in method '" + function.getName() 108 | + "' parameter named '" + paramName + "'. This is a potential SQL injection issue. Consider using parameters instead."); 109 | } 110 | } 111 | 112 | // PARAMETER OF THE FUNCTION IS A VARIABLE => CHECK IF CONST 113 | @Override 114 | public void visitPhpVariable(Variable variable) { 115 | checkReferencedVariable(variable.resolve()); 116 | } 117 | 118 | @Override 119 | public void visitPhpStringLiteralExpression(StringLiteralExpression expression) { 120 | if (ParameterHelper.checkElement(expression)) { 121 | logError(); 122 | } 123 | } 124 | 125 | // PARAMETER OF THE FUNCTION IS A CLASS FIELD => CHECK IF CONST 126 | @Override 127 | public void visitPhpFieldReference(FieldReference fieldReference) { 128 | checkReferencedVariable(fieldReference.resolve()); 129 | } 130 | 131 | // PARAMETER OF THE FUNCTION IS A CLASS METHOD CALL RESULTS => UNSAFE 132 | @Override 133 | public void visitPhpMethodReference(MethodReference reference) { 134 | PsiElement[] parameters = reference.getParameters(); 135 | for (PsiElement parameter : parameters) { 136 | if(ParameterHelper.checkElement(parameter)){ 137 | logError(); 138 | return; 139 | } 140 | } 141 | } 142 | 143 | // PARAMETER OF THE FUNCTION IS A FUNCTION CALL RESULTS => UNSAFE 144 | @Override 145 | public void visitPhpFunctionCall(FunctionReference reference) { 146 | PsiElement[] parameters = reference.getParameters(); 147 | for (PsiElement parameter : parameters) { 148 | if(ParameterHelper.checkElement(parameter)){ 149 | logError(); 150 | return; 151 | } 152 | } 153 | } 154 | 155 | private void checkReferencedVariable(PsiElement resolved) { 156 | if (ParameterHelper.checkElement(resolved)) { 157 | logError(); 158 | } 159 | } 160 | 161 | private void logError() { 162 | problemsHolder.registerProblem(argument, 163 | why + "In method '" + function.getName() + "', parameter named '" + paramName + 164 | "' seems not to be constant. This may allow an assailant to use SQL injection. Consider using parameterized query instead."); 165 | } 166 | } 167 | } --------------------------------------------------------------------------------