-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add new sql alchemy inspection skeleton * SQL alchemy check
- Loading branch information
1 parent
a11b56e
commit 0a6fd9c
Showing
10 changed files
with
233 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# SQL200 | ||
|
||
Looks for SQL injection in the SQLalchemy library. | ||
|
||
- Use of `text()` function to construct parameters on non-literal input | ||
- Use of the `.suffix_with()` and `.prefix_with()` methods on a query object with unsafe input | ||
|
||
## Examples | ||
|
||
Use of the SQLalchemy with a `text()` fragment can expose the constructed query to SQL injection. | ||
|
||
For example, this query should generate | ||
|
||
```python | ||
part = f"age<{age}" # exploitable, can override the original filter. | ||
_x = session.query(User).filter(User.username == user).filter(text(part)).all() | ||
``` | ||
|
||
With an input of `age = 224`: | ||
|
||
```sql | ||
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname FROM users WHERE users.id = ? AND age < 224 OR 1=1 | ||
``` | ||
|
||
If the `age` argument was `224 OR 1=1`, the query would bypass the filter: | ||
|
||
```sql | ||
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname FROM users WHERE users.id = ? AND id<224 OR 1=1 | ||
``` | ||
|
||
Both the `.suffix_with()` and `.prefix_with()` methods are vulnerable to unsafe input. | ||
|
||
```python | ||
suffix = " OR 1=1" # Example exploiting suffix to add/change WHERE clause | ||
prefix = " *," # Example exploiting query to get all fields | ||
stmt = select([users.c.name]).where(users.c.id == 1).suffix_with(suffix, dialect="sqlite") | ||
conn.execute(stmt) | ||
|
||
stmt2 = select([addresses]).prefix_with(prefix) # can be chained | ||
conn.execute(stmt2) | ||
``` | ||
|
||
Direct execution of vulnerable queries will be caught by SQL100: | ||
|
||
```python | ||
connection.execute("SELECT email_address FROM addresses WHERE email_address = \'{}\'".format(unsafe_input)) | ||
``` | ||
|
||
## Fixes | ||
|
||
Replace with native SQLalchemy queries using the API instead of creating direct SQL. | ||
|
||
## See Also | ||
|
||
[RealPython.com article on SQL injection](https://realpython.com/prevent-python-sql-injection/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/main/java/security/validators/SqlAlchemyUnsafeQueryInspection.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package security.validators | ||
|
||
import com.intellij.codeInspection.LocalInspectionToolSession | ||
import com.intellij.codeInspection.ProblemsHolder | ||
import com.intellij.psi.PsiElementVisitor | ||
import com.jetbrains.python.inspections.PyInspection | ||
import com.jetbrains.python.psi.PyCallExpression | ||
import com.jetbrains.python.psi.PyStringLiteralExpression | ||
import security.Checks | ||
import security.helpers.SecurityVisitor | ||
import security.helpers.calleeMatches | ||
import security.helpers.hasImportedNamespace | ||
import security.helpers.skipDocstring | ||
import security.registerProblem | ||
|
||
class SqlAlchemyUnsafeQueryInspection : PyInspection() { | ||
val check = Checks.SqlAlchemyUnsafeQueryCheck | ||
|
||
override fun getStaticDescription(): String? { | ||
return check.getStaticDescription() | ||
} | ||
|
||
override fun buildVisitor(holder: ProblemsHolder, | ||
isOnTheFly: Boolean, | ||
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session) | ||
|
||
private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : SecurityVisitor(holder, session) { | ||
|
||
override fun visitPyCallExpression(node: PyCallExpression) { | ||
if (skipDocstring(node)) return | ||
val targetFunctions = arrayOf("text", "prefix_with", "suffix_with") | ||
if (!calleeMatches(node, targetFunctions)) return | ||
if (!hasImportedNamespace(node.containingFile, "sqlalchemy")) return | ||
if (node.arguments.isNullOrEmpty()) return | ||
val sqlStatement = node.arguments.first() | ||
if (sqlStatement is PyStringLiteralExpression) return | ||
holder.registerProblem(node, Checks.SqlAlchemyUnsafeQueryCheck) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<h1>SQL200</h1> | ||
<p>Looks for SQL injection in the SQLalchemy library.</p> | ||
<ul> | ||
<li>Use of <code>text()</code> function to construct parameters on non-literal input</li> | ||
<li>Use of the <code>.suffix_with()</code> and <code>.prefix_with()</code> methods on a query object with unsafe input</li> | ||
</ul> | ||
<h2>Examples</h2> | ||
<p>Use of the SQLalchemy with a <code>text()</code> fragment can expose the constructed query to SQL injection.</p> | ||
<p>For example, this query should generate </p> | ||
<pre><code class="python">part = f"age<{age}" # exploitable, can override the original filter. | ||
_x = session.query(User).filter(User.username == user).filter(text(part)).all() | ||
</code></pre> | ||
<p>With an input of <code>age = 224</code>:</p> | ||
<pre><code class="sql">SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname FROM users WHERE users.id = ? AND age < 224 OR 1=1 | ||
</code></pre> | ||
<p>If the <code>age</code> argument was <code>224 OR 1=1</code>, the query would bypass the filter:</p> | ||
<pre><code class="sql">SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname FROM users WHERE users.id = ? AND id<224 OR 1=1 | ||
</code></pre> | ||
<p>Both the <code>.suffix_with()</code> and <code>.prefix_with()</code> methods are vulnerable to unsafe input.</p> | ||
<pre><code class="python">suffix = " OR 1=1" # Example exploiting suffix to add/change WHERE clause | ||
prefix = " *," # Example exploiting query to get all fields | ||
stmt = select([users.c.name]).where(users.c.id == 1).suffix_with(suffix, dialect="sqlite") | ||
conn.execute(stmt) | ||
|
||
stmt2 = select([addresses]).prefix_with(prefix) # can be chained | ||
conn.execute(stmt2) | ||
</code></pre> | ||
<p>Direct execution of vulnerable queries will be caught by SQL100:</p> | ||
<pre><code class="python">connection.execute("SELECT email_address FROM addresses WHERE email_address = \'{}\'".format(unsafe_input)) | ||
</code></pre> | ||
<h2>Fixes</h2> | ||
<p>Replace with native SQLalchemy queries using the API instead of creating direct SQL.</p> | ||
<h2>See Also</h2> | ||
<p><a href="https://realpython.com/prevent-python-sql-injection/">RealPython.com article on SQL injection</a></p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
src/test/java/security/validators/SqlAlchemyUnsafeQueryInspectionTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package security.validators | ||
|
||
import org.junit.jupiter.api.AfterAll | ||
import org.junit.jupiter.api.BeforeAll | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.TestInstance | ||
import security.Checks | ||
import security.SecurityTestTask | ||
|
||
@TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
class SqlAlchemyUnsafeQueryInspectionTest: SecurityTestTask() { | ||
@BeforeAll | ||
override fun setUp() { | ||
super.setUp() | ||
} | ||
|
||
@AfterAll | ||
override fun tearDown(){ | ||
super.tearDown() | ||
} | ||
|
||
@Test | ||
fun `verify description is not empty`(){ | ||
assertFalse(SqlAlchemyUnsafeQueryInspection().staticDescription.isNullOrEmpty()) | ||
} | ||
|
||
@Test | ||
fun `test unsafe text`() { | ||
var code = """ | ||
import sqlalchemy | ||
sqlalchemy.text(data) | ||
""".trimIndent() | ||
testCodeCallExpression(code, 1, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
|
||
@Test | ||
fun `test safe argument`() { | ||
var code = """ | ||
import sqlalchemy | ||
sqlalchemy.text(data) | ||
""".trimIndent() | ||
testCodeCallExpression(code, 0, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
|
||
@Test | ||
fun `test complex text`() { | ||
var code = """ | ||
import sqlalchemy | ||
session.query(User).filter(User.id == 1).filter(text(part)).all() | ||
""".trimIndent() | ||
testCodeCallExpression(code, 1, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
|
||
@Test | ||
fun `test unsafe suffix`() { | ||
var code = """ | ||
import sqlalchemy | ||
select([users.c.name]).where(users.c.id == 1).suffix_with(suffix, dialect="sqlite") | ||
""".trimIndent() | ||
testCodeCallExpression(code, 1, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
|
||
@Test | ||
fun `test text not sqlalchemy`(){ | ||
var code = """ | ||
import bicycle | ||
select([users.c.name]).where(users.c.id == 1).suffix_with(suffix, dialect="sqlite") | ||
""".trimIndent() | ||
testCodeCallExpression(code, 0, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
|
||
@Test | ||
fun `test sqlalchemy not text`(){ | ||
var code = """ | ||
import sqlalchemy | ||
vext(x) | ||
""".trimIndent() | ||
testCodeCallExpression(code, 0, Checks.SqlAlchemyUnsafeQueryCheck, "test.py", SqlAlchemyUnsafeQueryInspection()) | ||
} | ||
} |