Command Injection
Description
Gerapy before 0.9.3 has an OS command injection in the project_configure endpoint because user input is passed unsanitized to Popen.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Gerapy before 0.9.3 has an OS command injection in the project_configure endpoint because user input is passed unsanitized to Popen.
Vulnerability
CVE-2020-7698 is an OS command injection vulnerability in the Gerapy distributed crawler management framework (versions prior to 0.9.3). The root cause is that the project_configure endpoint passes user-controlled input directly to Popen via a shell command without sanitization [1]. The unsanitized parameter is project_name, which is concatenated into a gerapy generate command and executed with shell=True [2].
Exploitation
An attacker can exploit this vulnerability by sending a crafted request to the project_configure endpoint with a project_name containing shell metacharacters. The input is not filtered before being used in the command string. No authentication is required if the endpoint is exposed, and the attack can be performed remotely over the network. The GitHub commit fix adds a regex filter to remove dangerous characters like !, @, #, ;, &, *, ~, ", ', {, }, [, ], -, +, %, and ^ from the project_name before executing the command [2].
Impact
Successful exploitation allows an attacker to execute arbitrary OS commands on the server with the privileges of the Gerapy process. This could lead to full compromise of the server, including data exfiltration, installation of backdoors, or lateral movement within the network. The CVSS v3.1 base score is 9.8 (Critical) with an Attack Vector of Network and no privileges required [3].
Mitigation
Users should upgrade to Gerapy version 0.9.3 or later, which includes the fix that sanitizes the project_name input. For users unable to upgrade immediately, restricting network access to the project_configure endpoint and applying virtual patching or web application firewall rules can reduce risk. No public exploitation of this CVE has been reported in the KEV list.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
gerapyPyPI | < 0.9.3 | 0.9.3 |
Affected products
2Patches
14 files changed · +50 −69
gerapy/server/core/views.py+47 −63 modified@@ -1,3 +1,4 @@ +import re from pathlib import Path from urllib.parse import unquote import base64 @@ -251,6 +252,8 @@ def project_configure(request, project_name): configuration = json.dumps(data.get('configuration'), ensure_ascii=False) project.update(**{'configuration': configuration}) + # for safe protection + project_name = re.sub('[\!\@\#\$\;\&\*\~\"\'\{\}\]\[\-\+\%\^]+', '', project_name) # execute generate cmd cmd = ' '.join(['gerapy', 'generate', project_name]) p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) @@ -634,17 +637,15 @@ def job_list(request, client_id, project_name): if request.method == 'GET': client = Client.objects.get(id=client_id) scrapyd = get_scrapyd(client) - try: - result = scrapyd.list_jobs(project_name) - jobs = [] - statuses = ['pending', 'running', 'finished'] - for status in statuses: - for job in result.get(status): - job['status'] = status - jobs.append(job) - return JsonResponse(jobs) - except ConnectionError: - return JsonResponse({'message': 'Connect Error'}, status=500) + result = scrapyd.list_jobs(project_name) + jobs = [] + statuses = ['pending', 'running', 'finished'] + for status in statuses: + for job in result.get(status): + job['status'] = status + jobs.append(job) + return JsonResponse(jobs) + @api_view(['GET']) @@ -663,21 +664,18 @@ def job_log(request, client_id, project_name, spider_name, job_id): client = Client.objects.get(id=client_id) # get log url url = log_url(client.ip, client.port, project_name, spider_name, job_id) - try: - # get last 1000 bytes of log - response = requests.get(url, timeout=5, headers={ - 'Range': 'bytes=-1000' - }, auth=(client.username, client.password) if client.auth else None) - # Get encoding - encoding = response.apparent_encoding - # log not found - if response.status_code == 404: - return JsonResponse({'message': 'Log Not Found'}, status=404) - # bytes to string - text = response.content.decode(encoding, errors='replace') - return HttpResponse(text) - except requests.ConnectionError: - return JsonResponse({'message': 'Load Log Error'}, status=500) + # get last 1000 bytes of log + response = requests.get(url, timeout=5, headers={ + 'Range': 'bytes=-1000' + }, auth=(client.username, client.password) if client.auth else None) + # Get encoding + encoding = response.apparent_encoding + # log not found + if response.status_code == 404: + return JsonResponse({'message': 'Log Not Found'}, status=404) + # bytes to string + text = response.content.decode(encoding, errors='replace') + return HttpResponse(text) @api_view(['GET']) @@ -693,38 +691,29 @@ def job_cancel(request, client_id, project_name, job_id): """ if request.method == 'GET': client = Client.objects.get(id=client_id) - try: - scrapyd = get_scrapyd(client) - result = scrapyd.cancel(project_name, job_id) - return JsonResponse(result) - except ConnectionError: - return JsonResponse({'message': 'Connect Error'}) + scrapyd = get_scrapyd(client) + result = scrapyd.cancel(project_name, job_id) + return JsonResponse(result) @api_view(['GET']) @permission_classes([IsAuthenticated]) def del_version(request, client_id, project, version): if request.method == 'GET': client = Client.objects.get(id=client_id) - try: - scrapyd = get_scrapyd(client) - result = scrapyd.delete_version(project=project, version=version) - return JsonResponse(result) - except ConnectionError: - return JsonResponse({'message': 'Connect Error'}) + scrapyd = get_scrapyd(client) + result = scrapyd.delete_version(project=project, version=version) + return JsonResponse(result) @api_view(['GET']) @permission_classes([IsAuthenticated]) def del_project(request, client_id, project): if request.method == 'GET': client = Client.objects.get(id=client_id) - try: - scrapyd = get_scrapyd(client) - result = scrapyd.delete_project(project=project) - return JsonResponse(result) - except ConnectionError: - return JsonResponse({'message': 'Connect Error'}) + scrapyd = get_scrapyd(client) + result = scrapyd.delete_project(project=project) + return JsonResponse(result) @api_view(['POST']) @@ -829,18 +818,16 @@ def task_remove(request, task_id): :return: """ if request.method == 'POST': - try: - # delete job from DjangoJob - task = Task.objects.get(id=task_id) - clients = clients_of_task(task) - for client in clients: - job_id = get_job_id(client, task) - DjangoJob.objects.filter(name=job_id).delete() - # delete task - Task.objects.filter(id=task_id).delete() - return JsonResponse({'result': '1'}) - except: - return JsonResponse({'result': '0'}) + # delete job from DjangoJob + task = Task.objects.get(id=task_id) + clients = clients_of_task(task) + for client in clients: + job_id = get_job_id(client, task) + DjangoJob.objects.filter(name=job_id).delete() + # delete task + Task.objects.filter(id=task_id).delete() + return JsonResponse({'result': '1'}) + @api_view(['GET']) @@ -915,10 +902,7 @@ def render_html(request): url = unquote(base64.b64decode(url).decode('utf-8')) js = request.GET.get('js', 0) script = request.GET.get('script') - try: - response = requests.get(url, timeout=5) - response.encoding = response.apparent_encoding - html = process_html(response.text) - return HttpResponse(html) - except Exception as e: - return JsonResponse({'message': e.args}, status=500) + response = requests.get(url, timeout=5) + response.encoding = response.apparent_encoding + html = process_html(response.text) + return HttpResponse(html)
gerapy/__version__.py+1 −1 modified@@ -1,4 +1,4 @@ -VERSION = (0, 9, '3a3') +VERSION = (0, 9, '3b1') __version__ = '.'.join(map(str, VERSION))
README.md+0 −3 modified@@ -8,9 +8,6 @@   - -> 注:从 Gerapy 2.x 开始,其定位发生改变,不再支持 Scrapyd,转而支持 Docker、Kubernetes 的部署,另外开发还会迁移到 Scrapy 可视化配置和智能解析方面,敬请期待。 - Distributed Crawler Management Framework Based on Scrapy, Scrapyd, Scrapyd-Client, Scrapyd-API, Django and Vue.js. ## Documentation
requirements.txt+2 −2 modified@@ -5,12 +5,12 @@ django-cors-headers==3.2.0 django-apscheduler==0.3.0 furl==2.1.0 jinja2==2.10.1 -scrapy>=1.4.0 +scrapy==1.5.0 scrapy-redis==0.6.8 scrapy-splash==0.7.2 python-scrapyd-api==2.1.2 redis==2.10.5 -requests>=2.20.0 +requests==2.20.0 pymongo==3.9.0 pymysql==0.7.10 pyquery==1.2.17
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-g57j-q48p-9vm2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-7698ghsaADVISORY
- github.com/Gerapy/Gerapy/commit/e8446605eb2424717418eae199ec7aad573da2d2ghsax_refsource_MISCWEB
- github.com/pypa/advisory-database/tree/main/vulns/gerapy/PYSEC-2020-44.yamlghsaWEB
- snyk.io/vuln/SNYK-PYTHON-GERAPY-572470ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.