CVE-2021-41265 (Python, published 2021-12-09T17:15Z)
Description: value: Flask-AppBuilder is a development framework built on top of Flask. Verions prior to 3.3.4 contain an improper authentication vulnerability in the REST API. The issue allows for a malicious actor with a carefully crafted request to successfully authenticate and gain access to existing protected REST API endpoints. This only affects non database authentication types and new REST API endpoints. Users should upgrade to Flask-AppBuilder 3.3.4 to receive a patch.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-287 \u2014 Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
Repository: https://github.com/dpgaspar/Flask-AppBuilder
Files changed:
docs/config.rst
flask_appbuilder/security/api.py
flask_appbuilder/security/manager.py
flask_appbuilder/security/schemas.py
Commit message: chore: improve schema validation (#1712)



Vulnerable code:
from flask import request

from ..api import BaseApi, safe
from ..const import (
    API_SECURITY_ACCESS_TOKEN_KEY,
    API_SECURITY_PASSWORD_KEY,
    API_SECURITY_PROVIDER_DB,
    API_SECURITY_PROVIDER_KEY,
    API_SECURITY_PROVIDER_LDAP,
    API_SECURITY_REFRESH_KEY,
    API_SECURITY_REFRESH_TOKEN_KEY,
    API_SECURITY_USERNAME_KEY,
    API_SECURITY_VERSION,
)
from ..views import expose
    def login(self):
        username = request.json.get(API_SECURITY_USERNAME_KEY, None)
        password = request.json.get(API_SECURITY_PASSWORD_KEY, None)
        provider = request.json.get(API_SECURITY_PROVIDER_KEY, None)
        refresh = request.json.get(API_SECURITY_REFRESH_KEY, False)
        if not username or not password or not provider:
            return self.response_400(message="Missing required parameter")
        if provider == API_SECURITY_PROVIDER_DB:
            user = self.appbuilder.sm.auth_user_db(username, password)
        elif provider == API_SECURITY_PROVIDER_LDAP:
            user = self.appbuilder.sm.auth_user_ldap(username, password)
        else:
            return self.response_400(
                message="Provider {} not supported".format(provider)
        if refresh:
            resp[API_SECURITY_REFRESH_TOKEN_KEY] = create_refresh_token(
                identity=user.id
            )
    def refresh(self):
from typing import Dict, List, Optional, Set, Tuple
    def builtin_roles(self):
    def auth_type(self):
    def auth_username_ci(self):
    def auth_role_admin(self):
    def auth_role_public(self):
    def auth_ldap_server(self):
    def auth_ldap_use_tls(self):
    def auth_user_registration(self):
    def auth_user_registration_role(self):

Fixed code:
| AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS| Allow REST API login with alternative auth |   No      |
| True|False                             | providers (default False)                  |           |           |
+----------------------------------------+--------------------------------------------+-----------+
from flask import request, Response
from flask_appbuilder.api import BaseApi, safe
from flask_appbuilder.const import (
    API_SECURITY_ACCESS_TOKEN_KEY,
    API_SECURITY_PROVIDER_DB,
    API_SECURITY_PROVIDER_LDAP,
    API_SECURITY_VERSION,
)
from flask_appbuilder.security.schemas import login_post
from flask_appbuilder.views import expose
from marshmallow import ValidationError
    def login(self) -> Response:
        try:
            login_payload = login_post.load(request.json)
        except ValidationError as error:
            return self.response_400(message=error.messages)

        user = None
        if login_payload["provider"] == API_SECURITY_PROVIDER_DB:
            user = self.appbuilder.sm.auth_user_db(
                login_payload["username"], login_payload["password"]
            )
        elif login_payload["provider"] == API_SECURITY_PROVIDER_LDAP:
            user = self.appbuilder.sm.auth_user_ldap(
                login_payload["username"], login_payload["password"]
        if "refresh" in login_payload:
            login_payload["refresh"] = create_refresh_token(identity=user.id)
    def refresh(self) -> Response:
from typing import Any, Dict, List, Optional, Set, Tuple
        app.config.setdefault("AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS", False)
    @property
    def auth_type_provider_name(self) -> Optional[str]:
        provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
        return provider_to_auth_type.get(self.auth_type)

    def builtin_roles(self) -> Dict[str, Any]:
    def api_login_allow_multiple_providers(self):
        return self.appbuilder.get_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]

    @property
    def auth_type(self) -> int:
    def auth_username_ci(self) -> str:
    def auth_role_admin(self) -> str:
    def auth_role_public(self) -> str:
    def auth_ldap_server(self) -> str:
    def auth_ldap_use_tls(self) -> bool:
    def auth_user_registration(self) -> bool:
    def auth_user_registration_role(self) -> str:
from typing import Union

from flask import current_app
from flask_appbuilder.const import (
    API_SECURITY_PROVIDER_DB,
    API_SECURITY_PROVIDER_LDAP,
    AUTH_DB,
    AUTH_LDAP,
)
from marshmallow import fields, Schema, ValidationError
from marshmallow.validate import Length, OneOf


provider_to_auth_type = {"db": AUTH_DB, "ldap": AUTH_LDAP}


def validate_password(value: Union[bytes, bytearray, str]) -> None:
    if not value:
        raise ValidationError("Password is required")
    if len(value) == 1 and value.encode()[0] == 0:
        raise ValidationError("Password null is not allowed")


def validate_provider(value: Union[bytes, bytearray, str]) -> None:
    if not current_app.appbuilder.sm.api_login_allow_multiple_providers:
        provider_name = current_app.appbuilder.sm.auth_type_provider_name
        if provider_name and provider_name != value:
            raise ValidationError("Alternative authentication provider is not allowed")


class LoginPost(Schema):
    username = fields.String(required=True, allow_none=False, validate=Length(min=1))
    password = fields.String(
        validate=validate_password, required=True, allow_none=False
    )
    provider = fields.String(
        validate=[
            OneOf([API_SECURITY_PROVIDER_DB, API_SECURITY_PROVIDER_LDAP]),
            validate_provider,
        ]
    )
    refresh = fields.Boolean(required=False)


login_post = LoginPost()