CVE-2023–27524: Authentication Bypass in Apache Superset

vsociety
14 min readJun 11, 2024

--

by@jakaba

Screenshots from the blog posts

Summary

Apache Superset versions up to and including 2.0.1 are susceptible to a critical session validation vulnerability.

Description

Summary

Apache Superset, a widely used open-source tool for data visualization and exploration, has been identified as having potential security weaknesses that could lead to authentication bypass and remote code execution (RCE). These vulnerabilities could empower malicious actors to acquire administrative privileges on the targeted servers, allowing them to gather user credentials and potentially compromise data.

The specific security issue in question is an insecure default configuration vulnerability referred to as CVE-2023–27524, and a rudimentary proof-of-concept exploit code (PoC) has already been made available on GitHub.

The root cause lies in the predictable Flask Secret Key set during installation, affecting a substantial number of exposed Apache Superset instances. While some mitigation efforts have been made, this vulnerability remains a critical concern for those who have not updated their configurations.

In this analysis, we delve into the technical intricacies of CVE-2023–27524, highlighting its risks and the ongoing efforts to address them.

Introduction

Apache Superset versions up to and including 2.0.1 are susceptible to a critical session validation vulnerability. Installations that have not modified the default SECRET_KEY configuration as per installation instructions are at risk. Attackers can exploit this vulnerability to authenticate and gain access to unauthorized resources. Superset administrators who have changed the default SECRET_KEY value are not affected by this vulnerability.

This vulnerability poses a significant risk as it allows attackers to bypass session validation, potentially compromising the confidentiality, integrity, and availability of the Apache Superset instance. Organizations using default SECRET_KEY configurations should take immediate steps to mitigate this critical issue.

What is Apache Superset?

Apache Superset is an open-source data visualization and data exploration web application designed to help organizations and data analysts create interactive and shareable dashboards for exploring and visualizing data. It provides a user-friendly interface that allows users to connect to various data sources, create charts and graphs, and design interactive dashboards to gain insights from their data.

Superset is written in Python and based on the Flask web framework.

About the popularity: It has over 50K stars on GitHub, and there are more than 3000 instances of it exposed to the Internet. The usage of Superset over the last year has increased.

Impact and severity

Overall, the CVSS metrics for CVE-2023–27524 indicate that it is a critical vulnerability with a high potential for exploitation, significant impacts on confidentiality, integrity, and availability, and it can be exploited remotely without requiring special privileges or user interaction.

The vulnerability poses a serious threat by allowing unauthorized access to servers with dangerous default configurations. This can result in data exposure, unauthorized modifications, credential theft, and the execution of malicious code. It significantly compromises server security, requiring prompt remediation and close monitoring for signs of compromise.

Additionally, according to a report by Horizon3.AI, “…a substantial portion of these servers — at least 2000 (two-thirds of all servers) — are running with a dangerous default configuration.” Consequently, a substantial number of these servers are essentially accessible to anyone.

All in all, organizations should take this vulnerability seriously and apply appropriate mitigation measures promptly.

Lab Setup

Installing Superset Locally Using Docker Compose

Here is the official guideline, but of course, we run through it.

Install Docker and Docker Compose

Follow these steps:

After installation, verify everything went successful:

Clone Superset’s GitHub repo

You can get the last vulnerable version with this command:

git clone https://github.com/apache/superset.git

After the successful completion of that command, a fresh “superset” directory should become visible in your present directory.

Launch Superset

Navigate to the folder you created:

cd superset

Switch to a vulnerable version.

git checkout 2.0.1

Pull and run a specific version of Superset:

TAG=2.0.1 docker-compose -f docker-compose-non-dev.yml pull

Then, start the instances.

TAG=2.0.1 docker-compose -f docker-compose-non-dev.yml up

Now open http://localhost:8088 and log in as admin with password admin.

Now, in the menu, we can verify the version:

Install Apache Superset from scratch

To install Superset from scratch, you can follow the official guideline.

I used Kali Linux.

Update the package list information

sudo apt update

Install required dependencies

sudo apt-get install -y build-essential libssl-dev libffi-dev python-dev-is-python3 pip libsasl2-dev libldap2-dev default-libmysqlclient-dev

Install Python virtual environment

apt install python3-venv

Create and activate a virtual environment

cd /opt
python3 -m venv superset
source superset/bin/activate
cd superset
source bin/activate

Once you activate your virtual environment, all of the Python packages you install or uninstall will be confined to this environment. You can exit the environment by running deactivate on the command line.

Install Superset via PIP

Install the last vulnerable version of apache-superset via PIP packet manager:

pip install apache-superset==2.0.1

Initialize the database:

superset db upgrade

Then, finish the installation:

# Create an admin user in your metadata database (use `admin` as username to be able to load the examples)
export FLASK_APP=superset
superset fab create-admin

# Load some data to play with
superset load_examples

# Create default roles and permissions
superset init

# Build javascript assets
cd superset-frontend
npm ci
npm run build
cd ..

Finally, start the dev server on port 8088:

superset run -p 8088 --with-threads --reload --debugger

Vulnerability reproduce

In short, the first part of the exploiting is easy. In the absence of single sign-on protection for the Superset server, an attacker has the potential to manipulate a session cookie by utilizing the flask-unsign toolkit, setting the user_id value to 1. Typically, the initial Superset user is designated as an administrator, and if the attacker successfully places the manipulated session cookie in the browser's local storage, they gain unauthorized access to the application as an administrator. Conversely, when the Superset server is shielded by a single sign-on mechanism, finding a valid user_id value may necessitate additional effort, and the feasibility of this attack route remains untested.

However, escalating the vulnerability to remote command execution (RCE) requires further investigations.

Check the presence of the vulnerability

Let’s check if our Apache Superset server is running with an insecure default configuration with Horizon3’s tool.

# Clone the repo
cd /opt/superset/
git clone https://github.com/horizon3ai/CVE-2023-27524.git
cd CVE-2023-27524

# Install requirements
pip install -r requirements.txt

# Run the script
python3 CVE-2023-27524.py --url localhost:8088

Of course, it turns out this is vulnerable.

Now you can set the “session” cookie’s value to the forget one and refresh the page to get logged in as admin.

Exploitation

Now run it with proof of exploitability:

python3 CVE-2023-27524.py --url localhost:8088 --validate

The --validate flag is to validate exploitability by enumerating databases using the Superset API.

The repo’s README says the following about troubleshooting:

If the script detects a SECRET_KEY in use but is not able to validate exploitability, it could be because:
- target server is integrated with SSO and user ids don't follow the auto-increment format (try the --id argument to set a specific user_id if you know it)
- no user has been configured yet
- time drift between target and your machine. Flask session cookies are signed with a timestamp element. Try increasing the 5 second sleep interval in code.

The exploit script

Here is a brief breakdown of how the “exploit” script you can see here is built up:

  1. It imports several Python libraries, including flask_unsign, requests, urllib3, argparse, and re.
  2. It defines a list of SECRET_KEYS, which are used in an attempt to forge session cookies.
  3. The main() function is the entry point of the script and handles the main logic.
  4. The script takes command-line arguments using the argparse library. These arguments include the base URL of the Superset instance, a user ID to forge a session cookie for, an option to validate login, and a timeout value.
  5. It makes an HTTP GET request to the Superset login page using the requests library. It extracts the session cookie from the response.
  6. It decodes the session cookie using the flask_unsign library to check if it's a Flask session cookie.
  7. It extracts the Superset version from the HTTP response.
  8. It attempts to crack the session cookie using a list of predefined SECRET_KEYS.
  9. If it successfully cracks the session cookie, it forges a new session cookie for a specified user ID.
  10. If the --validate option is provided, it tries to validate the forged cookie by making an HTTP GET request to the login page with the forged cookie. It checks if the response status code is 302 (indicating a successful login).
  11. If validation is successful, it proceeds to enumerate databases associated with the Superset instance.

Escalating to RCE

The author of the Horizon3 article shows it is possible to escalate this issue to RCE but does not tell us how:

“We are not disclosing any exploit methods at this time, though we think it’ll be straightforward for interested attackers to figure it out.”

Static analysis

Superset employs a security mechanism involving cryptographically signed session cookies for managing user states. It is a common practice in Flask.

Here’s a technical breakdown of how this process works and the security implications:

  1. Session cookie creation: When a user logs into the Superset web application, the server sends a session cookie back to the user’s browser. This cookie contains a user identifier and is cryptographically signed to ensure its authenticity.
  2. Cryptographic signing: The session cookie is signed using a secret key, referred to as the SECRET_KEY. This key should be randomly generated and securely stored in a local configuration file. The signing process involves applying a cryptographic hash to the cookie's content.
  3. User authentication: With each subsequent web request, the user’s browser includes the signed session cookie. The Superset application validates the cookie’s signature to verify the user’s identity before processing the request.
  4. Security dependency: The security of the web application relies heavily on keeping the SECRET_KEY confidential. If this key is ever exposed, an attacker without prior privileges could generate and sign their own cookies, potentially gaining unauthorized access to the application while posing as a legitimate user. The tool known as flask-unsign streamlines the process of deciphering a session cookie to determine whether it was authenticated with a vulnerable SECRET_KEY, and subsequently fabricating a counterfeit yet legitimate session cookie using a confirmed SECRET_KEY.
  5. Risk of exposure: It was discovered that the default SECRET_KEY value used during Superset installation was not secure. Instead of a cryptographically strong key, it defaulted to a weak value: "\x02\x01thisismyscretkey\x01\x02\e\y\y\h." It is the responsibility of end users to modify the application's configuration and replace this default key with a strong, randomly generated one, as outlined in the Superset configuration guide.

In summary, Superset relies on cryptographically signed session cookies for secure user state management. However, the security of this mechanism hinges on maintaining the secrecy of the SECRET_KEY. Failure to replace the default key with a strong, randomly generated one can expose web applications to significant security risks, as highlighted in the research findings.

Patch diffing

In January 2022 the SECRET_KEY value was changed to a new default, which is "CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET," and a corresponding warning was included in the logs via a Git commit.

This change serves as the patch to address the security issue by implementing a more secure default SECRET_KEY value and raising awareness of its importance.

Let’s break down the changes.

Changes in the CLI

The patch adds a new CLI command to help users migrate their current secrets.

They have to set PREVIOUS_SECRET_KEY with their old SECRET and set their new secret with SECRET_KEY then run superset re-encrypt-secrets.

Here is the related git diff:

A summary of this improvement:

  1. Secrets Handling: The new version of superset-cli.py introduces a re_encrypt_secrets command that deals with secrets migration. It allows for re-encrypting secrets with a new key. This is a security improvement as it helps in managing secrets securely and can be useful for rotating secrets, which is a recommended security practice.
  2. Encryption: The SecretsMigrator class is used to manage secrets and facilitate their re-encryption. This is a security enhancement as it ensures that secrets are handled more securely and allows for transitioning to a more robust encryption mechanism if needed.
  3. Error Handling: The new version includes error handling for cases where an invalid previous secret key is provided. This helps prevent issues and potential vulnerabilities that may arise from using an incorrect or invalid secret key during re-encryption.

Overall, the improvements in the new version focus on enhancing the security of secret management within the Superset application, making it more resilient to potential security threats and vulnerabilities.

Let’s see the SecretsMigrator in superset/utils/encrypt.py too that is responsible for handling secrets migration within a Superset application. Let's break down the functionality and analyze its structure:

  1. Initialization (__init__ method):
  • The class constructor accepts one parameter, previous_secret_key, which is a string representing the previous secret key used for encryption.
  • It imports the db module from the Superset package and assigns it to the instance variable _db.
  • It also initializes the _previous_secret_key attribute with the provided previous_secret_key.
  • The dialect of the database engine used in Superset is determined and stored in the _dialect attribute.

2. discover_encrypted_fields method:

  • This method is responsible for iterating over the SQLAlchemy metadata to identify columns that use the EncryptedType.
  • It builds a dictionary called meta_info where keys are table names, and values are dictionaries containing column names and their corresponding EncryptedType instances.
  • The method returns meta_info, which provides information about encrypted columns in the database.

3. Private Helper Methods (_read_bytes, _select_columns_from_table):

  • _read_bytes is a static method that attempts to convert a value to bytes based on its type.
  • _select_columns_from_table retrieves specific columns from a table using a database connection.

4. _re_encrypt_row method:

  • This method is used to re-encrypt all columns in a row of a table.
  • It iterates over the columns and re-encrypts each one, updating the row with the new encrypted values.
  • If the previous secret key cannot decrypt a value, it checks if the current secret key can decrypt it. If successful, it logs a message and returns.

5. run method:

  • The run method is the main entry point for the secrets migration process.
  • It first collects information about tables and encrypted columns using the discover_encrypted_fields method.
  • It then iterates over the tables, retrieves rows, and calls _re_encrypt_row to re-encrypt the data.
  • The entire process is wrapped in a transaction to ensure data consistency.
  • It logs messages to provide information on the progress of the migration.

Changes in docs

The PR summary says:

The default SECRET_KEY changes to a more standard default, making it more obvious that a change is needed also. This can break current installations that were relying on the previous default SECRET_KEY I believe this is reasonable, since this is a dangerous setting and a proper setting is imperative.

The documentation has also changed. Now it says about the SECRET_KEY the following: "Use a strong complex alphanumeric string and use a tool to help you generate a sufficiently random sequence, ex: openssl rand -base64 42.

The app secret is modified in the Superset config file with an unambiguous suggestion:

Additionally, an alert pops up during the initialization when the app detects that the secret key is the default:

So, when the default SECRET_KEY is detected, Superset will log:

superset_app             | --------------------------------------------------------------------------------
superset_app | WARNING
superset_app | --------------------------------------------------------------------------------
superset_app | A Default SECRET_KEY was detected please use superset_config.py to override it.
superset_app | Use a strong complex alphanumeric string and use a tool to help you generate
superset_app | a sufficiently random sequence, ex: openssl rand -base64 42
superset_app | --------------------------------------------------------------------------------
superset_app | --------------------------------------------------------------------------------

Another patch

In the 2.1 release, the Superset team implemented a change that prevents the server from starting if it is configured with a default SECRET_KEY. This update serves as a safeguard, ensuring that new Superset users won't inadvertently encounter issues related to insecure configurations.

However, it’s important to note that this fix is not entirely foolproof. There are scenarios where Superset can still be run with a default SECRET_KEY, particularly when it's installed using a docker-compose file or a helm template. In these cases, the docker-compose file includes a new default SECRET_KEY called TEST_NON_DEV_SECRET, which some users may unintentionally use when running Superset. Additionally, certain configurations set admin/admin as the default credentials for the admin user.

Mitigation

First, ensure your Apache Superset installation is up-to-date with the latest version and the accompanying security patch. Verify the patch’s effectiveness in addressing the default configuration issue. Additionally, review system configurations for other potential vulnerabilities.

Horizon3.AI’s research found that a significant portion of Superset servers on the internet still use a default and weak key. This is concerning because it indicates that many users may not have followed the recommended security practices, potentially leaving their applications vulnerable to unauthorized access and session cookie tampering. To mitigate this, users should promptly change the default SECRET_KEY provided during Superset installation to enhance security. They should replace it with a strong, randomly generated, and cryptographically secure secret key. Check the Configuring Superset Guide.

The superset CLI can also automate the process of rotating secrets – see here.

They also mention:

We have not validated exploitation against Superset installs with single sign-on (SSO) configured. SSO may make it hard to forge session cookies if the user_ids are unpredictable GUIDs rather than auto-incrementing identifiers. At the same time, it’s possible there are other attack paths that leak user ids, or the user ids map to easily discoverable identifiers such as email addresses. We recommend remediating even if your Superset install is behind SSO.

Checking vulnerability

If you’re a Superset user, you can assess the vulnerability of your server using a script available on Horizon3’s GitHub repo. This script leverages the flask-unsign toolkit to examine whether the Superset session cookie has been signed with any of the recognized default SECRET_KEYs. If the script indicates that your Superset instance is vulnerable, and you have a Superset instance running on the Internet, it is strongly advised to take immediate action to address the issue or remove your instance from the Internet.

Final thoughts

My exploration into the world of Flask, secret management, and security vulnerabilities led me down a fascinating path. It’s intriguing to note that the issue of hardcoded Flask secret keys is not unique to Superset.

A fundamental truth we encountered during this journey is that users often don’t read documentation, and applications should be designed to guide users toward secure practices by default. The data we collected reinforces this wisdom. While we all recognize that default credentials and keys pose risks, the extent of their impact, as seen in Superset, affecting roughly two-thirds of users, underscores the need to remove choices that could lead to insecurity. Requiring deliberate actions for insecurity can be a powerful way to safeguard against common pitfalls in the realm of security.

Resources

Ákos Jakab
Security researcher

--

--

vsociety
vsociety

Written by vsociety

vsociety is a community centered around vulnerability research

No responses yet