FREE: Secure Your Gravity Forms Stripe Keys – Fortress Vaults & Pillars in Practice

| in


Introduction

In this guide, we’ll walk you through the process of securing your Gravity Forms Stripe keys using Fortress Vaults & Pillars. By the end of this tutorial, your sensitive Stripe keys will be securely encrypted, and the risk of malicious access will be significantly reduced.

We will be using the following Software versions:

Note: This tutorial is part of a series demonstrating how to implement Fortress Vaults & Pillars across various plugins. If you’re unfamiliar with Fortress Vaults & Pillars, you may first want to read our comprehensive introduction to understand the full context and learn how to get FREE access.

The Status Quo

In Gravity Forms, sensitive Stripe keys are stored in plaintext within the WordPress options table. This practice leaves your site vulnerable to malicious attacks, potentially exposing confidential information to unauthorized users if any plugin or theme on your site contains SQL-Injection vulnerabilities.

It’s essential to recognize that Gravity Forms does not utilize restricted Stripe keys as of the current version. This leaves the compromise of API keys an extremely critical vulnerability with potentially devastating consequences.

If an attacker were to access these keys, they might be able to:

  • Commit Fraud: An attacker can initiate fraudulent transactions by gaining access to your Stripe keys. A recent example is a WordPress Agency that experienced a $70k fraud due to compromised Stripe keys.
  • Delete Customers & Subscriptions: A disgruntled hacker could erase all your valuable customer data and subscriptions, causing existential disruption to your business.
  • Gain Unauthorized Access to Sensitive Data: Stripe keys grant unrestricted access to all data in your account, such as sensitive customer data, leading to potential privacy breaches and legal liabilities.
A screenshot of the Gravity Forms Stripe Settings that shows the connection to Stripe.
Gravity Forms uses Stripe’s OAuth Flow to get access to the API keys.

After connecting our Stripe Account to Gravity Forms, we can assert that our API keys are indeed stored as plaintext in the wp_options table.

wp option list --search=gravityformsaddon_gravityformsstripe_settings* --unserialize --format=json | jq '.[].option_value'
{
  "api_mode": "test",
  "live_publishable_key_is_valid": "",
  "live_secret_key_is_valid": "",
  "test_publishable_key_is_valid": "1",
  "test_secret_key_is_valid": "1",
  "stripe_admin_init_intent": "",
  "test_publishable_key": "pk_test_51HFiwWH81jF8atYWUTqZOr7TBRbFlkrOuiqeuFILVICnyfBJSgSWFRfbP55tRyipamXHRWZ5P4Zvf5aKW7SiDO1j00mzUxLED7",
  "test_secret_key": "sk_test_51HFiwWH81jF8atYWeZgk4emuoQIP3Ba36J3vEtCueYKSghXZzYK0iNl9auW2rJXlljMu2rtX90GXGLhTxUM4Yiwu00YudvNbDa",
  "live_publishable_key": "",
  "live_secret_key": "",
  "webhooks_enabled": "1",
  "test_signing_secret": "whsec_MKS0kc9NogI4oV8AmjabiFmJHeatr2uE",
  "live_signing_secret": "",
  "checkout_method": "stripe_elements",
  "test_auth_token": {
    "stripe_user_id": "acct_XXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "refresh_token": "rt_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "date_created": 1691677288
  }
}

Step-by-Step Guide to Adding Vaults

We’ll now walk you through using Fortress Vaults & Pillars to secure the required sensitive data within the plugin.

Note: If you’re unfamiliar with Fortress Vaults & Pillars, refer to the announcement and check the developer documentation for more information.

Finding the Correct WordPress Option

Jump to the final configuration.

Before securing sensitive data, we must identify the specific WordPress option where Gravity Forms stores the Stripe keys. There are several ways to do this, but we will use WP-CLI for our demonstration.

First, list all option names starting with “gravityforms” with the following command:

wp option list --search=gravityforms* --unserialize --format=json | jq '.[].option_name'

The output will be something like this:

"gravityformsaddon_feed-base_version"
"gravityformsaddon_gravityformsstripe_settings"
"gravityformsaddon_gravityformsstripe_version"
"gravityformsaddon_gravityformswebapi_version"
"gravityformsaddon_payment_addons"
"gravityformsaddon_payment_version"

Here, we’ve identified the correct option name:

gravityformsaddon_gravityformsstripe_settings

Now, we can find the correct sub key for the Vaults by running:

wp option list --search=gravityformsaddon_gravityformsstripe_settings* --unserialize --format=json | jq '.[].option_value'

The result will be a JSON object showing the keys and values:

{
  "api_mode": "test",
  "live_publishable_key_is_valid": "",
  "live_secret_key_is_valid": "",
  "test_publishable_key_is_valid": "1",
  "test_secret_key_is_valid": "1",
  "stripe_admin_init_intent": "",
  "test_publishable_key": "pk_test_51HFiwWH81jF8atYWUTqZOr7TBRbFlkrOuiqeuFILVICnyfBJSgSWFRfbP55tRyipamXHRWZ5P4Zvf5aKW7SiDO1j00mzUxLED7",
  "test_secret_key": "sk_test_51HFiwWH81jF8atYWeZgk4emuoQIP3Ba36J3vEtCueYKSghXZzYK0iNl9auW2rJXlljMu2rtX90GXGLhTxUM4Yiwu00YudvNbDa",
  "live_publishable_key": "",
  "live_secret_key": "",
  "webhooks_enabled": "1",
  "test_signing_secret": "whsec_MKS0kc9NogI4oV8AmjabiFmJHeatr2uE",
  "live_signing_secret": "",
  "checkout_method": "stripe_elements",
  "test_auth_token": {
    "stripe_user_id": "acct_XXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "refresh_token": "rt_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "date_created": 1691677288
  }
}

We highlighted all sensitive sub-keys stored as part of the option.

Creating the Final Configuration

Now that we identified all sensitive parts of the Gravity Forms Stripe Addon, we can use the following configuration to turn all of them into Vaults:

{
  "vaults_and_pillars": {
    "option_vaults": {
      "gravityformsaddon_gravityformsstripe_settings.test_secret_key": {},
      "gravityformsaddon_gravityformsstripe_settings.live_secret_key": {},
      "gravityformsaddon_gravityformsstripe_settings.test_signing_secret": {},
      "gravityformsaddon_gravityformsstripe_settings.live_signing_secret": {},
      "gravityformsaddon_gravityformsstripe_settings.test_auth_token": {},
      "gravityformsaddon_gravityformsstripe_settings.live_auth_token": {}
    }
  }
}

Note: Refer to the developer documentation for more information.

The Final Result

All sensitive Stripe data is now stored securely encrypted.

{
  "api_mode": "test",
  "live_publishable_key_is_valid": "",
  "live_secret_key_is_valid": "",
  "test_publishable_key_is_valid": "1",
  "test_secret_key_is_valid": "1",
  "stripe_admin_init_intent": "",
  "test_publishable_key": "pk_test_51HFiwWH81jF8atYWUTqZOr7TBRbFlkrOuiqeuFILVICnyfBJSgSWFRfbP55tRyipamXHRWZ5P4Zvf5aKW7SiDO1j00mzUxLED7",
  "test_secret_key": "FORTRESS_VAULT|MUIEAAqpAW57EkRStNjPcepEwum65CZk-nuDP3_LTlanbQ__UXykxTZPWLlhzBTp5IyG8zl9PlyvMkjtHL1MEeEFRkzlDwCUo5rvEzs6dUV1CH1S1kGWu6GKRiNoWzqow5GZCTBhfyQ5KrfxoaJg3DFpoqhD0LaiuD1THakAvc88HgiCDZqmN-rRCt-6LDgeLbrxYYWbq4Uchzyyuefe72POuedMwEoLjV19JkFS_1QE1ji_ozVesi7n_Qcqp3PBWjeEX56ReftxJDMD0bSwMJtWHLEOTiHP7lY0wMPq_nDWB_lBAQl3lyQXhSDIaySM",
  "live_publishable_key": "",
  "live_secret_key": "FORTRESS_VAULT|MUIEAMnaMMnQICwvyfWtwY5K3YHxlsOpq7JgfHhjakOyCMeaihWjTtpEZdCW8AiI6JPE4FRF1KUZ_UXZWwz2pqiLMgzZZLRa9x1mHI_CMqs5pg77lMLZCE2QndT5hP1Pu1Y6k-jmNJLKnNyk97Uz3XL6dF0cngalzedqB9R2GG4W-Ac=",
  "webhooks_enabled": "1",
  "test_signing_secret": "FORTRESS_VAULT|MUIEAHI34usTxzmm_Q-R8DB2PTx1IXpEASePR0A7fB7Loy0Ql_v-FMckyEsLXlcRD_qGVOuqkqfGh8rwPK0ofJajLZKkpScCWzujXgpD3ytV3FU_k4VHqnjjho7nhi4TVDqlGoin1GNJIB7Sjat2tSxGg-aEA5u35e9EdSSYq2SFMLh84l3Cbp1XQzIAf_C_iDULKZ_tJOlpIk4t2wHsPURqUenjI7k_0Cc=",
  "live_signing_secret": "FORTRESS_VAULT|MUIEAA1BFurkb3-5mE9Rs3ytRNnD7tJTU0vhlnymfAlF_Z9IKNKwtu2FLbbsKDqAe-vPmJpCjZKLUoRfMXSuwqzPXgjpwfxxjvVTlOMEMxZpiNlE_FTZFn-locTdTb4-i0Zl3ZPyz3di3t88bn7n6TGKqz79mHqum2NdleotgC_dNbU=",
  "checkout_method": "stripe_elements",
  "test_auth_token": "FORTRESS_VAULT|MUIEALZpeTSZDzEjMR-ZLGng5ftqpT59F1L6bio9MHrfdjWwX_TFmaXv3P3pAQOg-s3HLb2RYnOacw8v_FXE_4JlsIlicYwrgx2MX4P1WYGrVRJsaTnLXEvyg7YIqc3-D7gAQ9PcAkUJz_1OSanye3zAsOVhdTU9XBqGBC1FXqRsdBxdVoPUKjl9YKx8gPlMXUf6mMHAD07gVuxfNr4OllcekwAPGAI_lsfVLjREAteyTQfRc7wrpt8-yRuw6s2i_WkmU5V1QFfXYbOoRXn_rck39s7Ff8VMEwuto6tJRfN-S6V5Fo-Q1QjiKr297VuWKp4SHaiq4Asf3w-3e87aT4fY1_7rOXw6EE7vPVTH9778Gyb_5k-mrYXDoGtrEFsQ3ajMudlk"
}

Let’s now verify that everything, indeed, still works by:

  1. Creating a purchase form.
  2. Submitting the form with Stripe’s test credit cards.
  3. Asserting the results.
A screenshot of a basic Gravity Forms form with a Stripe Integration.
A basic Gravity Form with a Stripe Purchase field.
A screenshot showing that the Gravity Form submitting correctly.
The form was submitted successfully.
A screenshot of the connected Stripe Account that verifies that the purchase did arrive in Stripe.
The purchase went to our Stripe account despite the encrypted API keys.

Bonus: Hiding Sensitive Data in the UI

Gravity Forms recognizes the sensitivity of the Stripe webhook signing secrets, displaying them in a “masked” within the admin user interface.

A screenshot of the Gravity Forms Stripe settings page that shows that the WebHook signing secret is stored in the page's HTML.
Chaning the form field type from “password” to “text” with the browser’s dev tools exposes the secret.

However, this is achieved using a password form field that relies on the browser to display the value as “*******”.

Unfortunately, this approach still exposed the secret in the page’s HTML, which can easily be accessed using the browser’s developer tools.

Vaults&Pillars contains a hook that we can use to sanitize the webhook secrets selectively, by adding the below snippet/plugin.

Note: Refer to the developer documentation for more information.

<?php
/*
 * Plugin Name: Vaults & Pillars: Hide Gravity Forms Webhook Secret
 * Description: Hides the Gravity Forms Stripe Webhook Secret in the settings tab.
 * Author: Snicco
 * Author URI: https://snicco.io
 * LICENSE: MIT
 * Version: 1.0.0
 */
declare(strict_types=1);

use Snicco\Enterprise\Fortress\VaultsAndPillars\Core\Event\ReturningOptionRuntimeValue;

add_filter(ReturningOptionRuntimeValue::class, function (ReturningOptionRuntimeValue $event): void {
    
    // Don't do anything for other options.
    if ($event->option_name !== 'gravityformsaddon_gravityformsstripe_settings') {
        return;
    }

    // Only mask the secret for GET requests to the GravityForms settings tab.
    if (
        ! str_contains($_SERVER['REQUEST_URI'], 'page=gf_settings')
        || ! 'GET' === $_SERVER['REQUEST_METHOD']
    ) {
        return;
    }

    $test_webhook_secret = $event->wordpress_runtime_value['test_signing_secret'];
    $live_webhook_secret = $event->wordpress_runtime_value['live_signing_secret'];

    $event->wordpress_runtime_value['test_signing_secret'] = substr($test_webhook_secret, 0, 10).'_XXXXXXXXXXXXXXXXXXXXXXXX';
    $event->wordpress_runtime_value['live_signing_secret'] = substr($live_webhook_secret, 0, 10).'_XXXXXXXXXXXXXXXXXXXXXXXX';
});

This snippet masks the real webhook secret for GET requests to Gravity Form’s settings page.

A screenshot of the Gravity Forms Stripe settings page that shows that the WebHook signing secret is not stored in the page's HTML anymore.
Only the first ten characters are displayed now.

Get FREE Access to Fortress Vaults & Pillars and Secure Your Plugins Now!

Now that you’ve seen how Fortress Vaults & Pillars can add an essential layer of security to plugins without requiring codebase changes, it’s time to take action.

Explore our announcement post to learn more and get FREE access to Fortress Vaults here.