Vulnerability scanning on python project using Security SonarAnalyzer Python Repository not working

ALM used
GitLab

CI system used
GitLab CI

Scanner command used when applicable (private details masked)

  script:
    - git fetch origin master --depth 10
    - if [[ "$CREATE_RC" =~ 'TRUE' ]]; then
      RELEASE_VERSION=$(date --date="$(echo $CI_PIPELINE_CREATED_AT | tr -d 'TZ')" "+%Y%m%d%H%M%S");
      RELEASE_CANDIDATE_TAG="rc-$RELEASE_VERSION";
      sonar-scanner
      -Dsonar.projectVersion=$RELEASE_CANDIDATE_TAG
      -Dsonar.organization=Hidden
      -Dsonar.projectKey=$SONAR_PROJECT_KEY
      -Dsonar.sources=$SONAR_SOURCE
      -Dsonar.login=$SONAR_TOKEN
      -Dsonar.python.coverage.reportPaths=coverage.xml
      -Dsonar.python.xunit.reportPath=report.xml
      -Dsonar.python.bandit.reportPaths=bandit.txt
      -Dsonar.python.flake8.reportPaths=flake8.txt
      -Dsonar.python.version=3
      -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
      -Dsonar.typescript.tsconfigPaths=tsconfig.json
      -Dsonar.host.url=https://sonarcloud.io;
      else
      sonar-scanner
      -Dsonar.organization=Hidden
      -Dsonar.projectKey=$SONAR_PROJECT_KEY
      -Dsonar.sources=$SONAR_SOURCE
      -Dsonar.login=$SONAR_TOKEN
      -Dsonar.host.url=https://sonarcloud.io
      -Dsonar.python.coverage.reportPaths=coverage.xml
      -Dsonar.python.xunit.reportPath=report.xml
      -Dsonar.python.bandit.reportPaths=bandit.txt
      -Dsonar.python.flake8.reportPaths=flake8.txt
      -Dsonar.python.version=3
      -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
      -Dsonar.typescript.tsconfigPaths=tsconfig.json
      -Dsonar.qualitygate.wait=true;
      fi

Languages of the repository
Python

Only if the SonarCloud project is public, the URL
Private

And if you need help with pull request decoration, then the URL to the PR too
N/A

Error observed

We wanted to test that the vulnerability rules in our quality profile were triggered as expected when adding a vulnerable piece of code into the repo.

The error we have observed is vulnerabilities from the Security SonarAnalyzer repo are not being picked up by sonarcloud, whereas vulnerabilties from SonarQube Python repo are.

To prove this, we recreated the non-compliant code examples given in the SonarCloud rule definitions below.

SonarQube Python

  • python:S2115 - A secure password should be used when connecting to a database
  • python:S2053 - Hashes should include an unpredictable salt
  • python:S4830 - Server certificates should be verified during SSL/TLS connections
  • python:S5527 - Server hostnames should be verified during SSL/TLS connections

Security SonarAnalyzer

  • pythonsecurity:S5147 - NoSQL operations should not be vulnerable to injection attacks
  • pythonsecurity:S2076 - OS commands should not be vulnerable to command injection attacks
  • pythonsecurity:S3649 - Database queries should not be vulnerable to injection attacks
  • pythonsecurity:S5334 - Dynamic code execution should not be vulnerable to injection attacks

Steps to reproduce

  1. Create a sonarcloud project
  2. Create a python repository with the following code snippet
import os
from requests import request
from flask import Flask
from hashlib import pbkdf2_hmac
import ssl
import xml.etree.ElementTree as etree
import sqlite3
import boto3
# python:S4502 - FOUND
app = Flask(__name__)
def configure_app(app):
    # python:S2115 - FOUND
    app.config['SQLALCHEMY_DATABASE_URI'] = "postgresql://user:@domain.com"
def ping():
    # pythonsecurity:S2076 - NOT FOUND
    cmd = "ping -c 1 %s" % request.args.get("host", "www.google.com")
    status = os.system(cmd)
    return str(status == 0)
@app.route('/login')
def login():
    # python:S2053 - FOUND
    output_pbkdf2 = pbkdf2_hmac('sha256', '1232321312', b'D8VxSmTZt2E2YV454mkqAY5e', 100000)
    # python:S4830/python:S5527 - FOUND
    ctx = ssl._create_unverified_context()
    ctx.check_hostname = False
    parser = etree.XMLParser()
    tree1 = etree.parse('ressources/xxe.xml', parser)
    root1 = tree1.getroot()
    
    # pythonsecurity:S5147 - NOT FOUND
    dynamodb = boto3.client('dynamodb')
    username = request.args["username"]
    password = request.args["password"]
    dynamodb.scan(
        FilterExpression="username = " + username + " and password = " + password,
        TableName="users",
        ProjectionExpression="username"
    )
    
    # pythonsecurity:S3649 - NOT FOUND
    user = request.args["user"]
    sql = """SELECT user FROM users WHERE user = \'%s\'"""
    conn = sqlite3.connect('example')
    conn.cursor().execute(sql % (user))
    
    # pythonsecurity:S5334 - NOT FOUND
    operation = request.args.get("operation")
    eval(f"product_{operation}()")
    ping()
if __name__ == "__main__":
    app.run(debug=True)

As you can see rules pythonsecurity:S5147, pythonsecurity:S2076, pythonsecurity:S3649 and pythonsecurity:S5334 were all included we should also note that it includes the vulnerabilities from the SonarQube repo e.g. ‘python:S2115’ as well.

Four vulnerabilities (and 1 hotspot which was related to 1 of the vulnerabilities) were discovered. All 4 of these vulnerabilities were from the SonarQube Python repo - none from the Security SonarAnalyzer repo were found.

Our first thought was we must have a configuration error, however after exploring all the configuration and settings for SonarCloud (including creating a freetrial of an enterprise account to see the settings available at the organisation-level) we couldn’t find anything pertaining to the enabling/disabling of certain vulnerability repositories.

Potential workaround
No known workarounds.

Do not share screenshots of logs – share the text itself (bonus points for being well-formatted)!

Hi @robertjohnson22!

Thanks for reaching out. At Sonar, we really value our users’ feedback and we enjoy feedback like yours that help us build even better products.

I looked at the code sample you provided and, to me, there is actually no issue to raise. The request object you imported is the request function from the requests module, an HTTP client function. It differs significantly from the request attribute of the flask module.

To illustrate that, I slightly changed your code sample to this one.

import os
from requests import request
from flask import Flask

app = Flask(__name__)

@app.route('/ping')
def ping():
    # pythonsecurity:S2076 - NOT FOUND
    cmd = "ping -c 1 %s" % request.args.get("host", "www.google.com")
    status = os.system(cmd)
    return str(status == 0)

if __name__ == "__main__":
    app.run(debug=True)

If I now try to exploit the command injection vulnerability you introduced, Python returns the following AttributeError:

curl 127.0.0.1:5000/ping?host=a;echo+vulnerable
[SKIPED]
    cmd = "ping -c 1 %s" % request.args.get("host", "www.google.com")
AttributeError: 'function' object has no attribute 'args'

If I change the request import to use the request attribute from the flask module instead, the vulnerability gets exploitable.

import os
from flask import Flask, request

# python:S4502 - FOUND
app = Flask(__name__)

@app.route('/ping')
def ping():
    # pythonsecurity:S2076 - NOT FOUND
    cmd = "ping -c 1 %s" % request.args.get("host", "www.google.com")
    status = os.system(cmd)
    return str(status == 0)

if __name__ == "__main__":
    app.run(debug=True)
$ curl "127.0.0.1:5000/ping?host=a;echo+vulnerable"
----
[Log output]
ping: a: Temporary failure in name resolution
vulnerable
127.0.0.1 - - [27/Apr/2023 11:30:04] "GET /ping?host=a;echo+vulnerable HTTP/1.1" 200 -

I would advise that you try fixing this import and rerunning your analysis. When scanning that way on my side, all the expected issues are properly raised.

That said, I would be glad to hear which code example we provide uses the requests.request import. If there are none, I would still be interested in knowing which example does not provide the correct import information and might have led to your misunderstanding. This is something we would definitely want to fix.

I hope this help. Do not hesitate if there is anything else I can do.

Gaetan

3 Likes