Skip to content

Enable Feature Flags

In this tutorial, you will learn how to configure Match & Replace rules to gain access to features intended for admin users in an intentionally vulnerable application from Caido's Web Security Labs.

Many applications implement feature flag services to hide or display elements and conduct A/B testing for upcoming features.

Typically, access to these additional features is determined by Boolean values, user permission levels, or subscription tiers. However, when access checks are only performed client-side, they can be easily bypassed by creating Match & Replace rules to modify the response before it reaches the client.

Match & Replace Lab Walkthrough

NOTE

To access the lab, navigate to https://labs.caido.io/hubs/match-and-replace, sign in with your account credentials, and click on the Launch hub button.

The Match & Replace Lab displays different user-interface components based on a user's permission role: either basic or admin.

  1. With your proxy settings enabled, click on the Open in new tab button to launch your lab instance.
  2. Within the corresponding response is a user object:
js
        let user = { 
            name: "john", 
            role: "basic", 
            featureFlags: [] // Update this array to enable/disable features
        };
  1. If (user.role === "admin") in the checkUserRole() function:
js
        // Check if role is admin and update feature flags
        function checkUserRole() {
            if (user.role === "admin") {
                displayAdminUI();
            }
            
            displayUserInfo();
            checkFeatureFlags();
        }
  1. The displayAdminUI() function will execute and append an administrative panel to the page:
js
        // Display admin UI elements
        function displayAdminUI() {
            const adminContainer = document.createElement('div');
            adminContainer.id = 'admin-container';
            adminContainer.className = 'container';
            adminContainer.style.marginTop = '20px';
            adminContainer.style.backgroundColor = 'var(--card-bg)';
            adminContainer.style.border = '1px solid #333';

            const heading = document.createElement('h2');
            heading.textContent = 'Admin Controls';

            const adminButton = document.createElement('button');
            adminButton.textContent = 'Do admin action';
            adminButton.style.padding = '8px 16px';
            adminButton.style.backgroundColor = 'var(--button-bg)';
            adminButton.style.color = 'var(--text)';
            adminButton.style.border = 'none';
            adminButton.style.borderRadius = '4px';
            adminButton.style.cursor = 'pointer';

            adminButton.addEventListener('click', function() {
                fetch("/superSecretAdminStuff.php").then(a => a.json()).then(data => {
                    alert(data.message);
                });
            });

            adminContainer.appendChild(heading);
            adminContainer.appendChild(adminButton);
            document.body.appendChild(adminContainer);
        }
  1. The checkFeatureFlags() function will parse the user.featureFlags property array and append additional components to the user-interface:
js
        // Check feature flags and activate features
        function checkFeatureFlags() {
            // Check for bouncy ball feature
            if (user.featureFlags.includes('bouncy-ball')) {
                createBouncyBall();
            }

            // Check for sparkle background feature
            if (user.featureFlags.includes('sparkle-background')) {
                enableSparkleBackground();
            }
        }

        // Create and show bouncy ball
        function createBouncyBall() {
            const ball = document.createElement('div');
            ball.className = 'bouncy-ball';

            // Random starting position
            ball.style.left = Math.random() * 80 + 10 + '%';
            ball.style.top = Math.random() * 50 + 25 + '%';

            document.body.appendChild(ball);
        }

        // Enable sparkle background
        function enableSparkleBackground() {
            document.body.classList.add('sparkle-bg');

            // Create sparkles
            setInterval(createSparkle, 300);
        }

        // Create individual sparkle
        function createSparkle() {
            const sparkle = document.createElement('div');
            sparkle.className = 'sparkle';

            // Random position, size and color
            const size = Math.random() * 5 + 2;
            sparkle.style.width = size + 'px';
            sparkle.style.height = size + 'px';
            sparkle.style.left = Math.random() * 100 + '%';
            sparkle.style.top = Math.random() * 100 + '%';

            const colors = ['var(--accent)', '#FFC0CB', '#ADD8E6', '#90EE90', 'var(--text)'];
            sparkle.style.background = colors[Math.floor(Math.random() * colors.length)];

            document.body.appendChild(sparkle);

            // Fade out and remove
            setTimeout(() => {
                sparkle.style.opacity = '0';
                sparkle.style.transition = 'opacity 1s';
                setTimeout(() => {
                    document.body.removeChild(sparkle);
                }, 1000);
            }, 1000);
        }

Accessing the Administrative Panel

To display the administrative panel:

  1. Navigate to the Match & Replace interface and click on the + New Rule button.

TIP

Click on the button to rename the rule to a name that quickly identifies the rule's purpose such as "Change Role: basic to admin".

  1. Select Response Body from the Section drop-down menu and String from the Matcher drop-down menu.
  2. Type in basic in the Matcher input field and admin in the Replacer input field.
  3. Next, click on the Intercept checkbox to apply the modification to all proxied responses.
  4. Click on the Update button to add the rule to the Default Collection.
  5. Expand the Default Collection by clicking on the button attached to it and click on the rule's associated checkbox to enable it.
Creating a the role change rule.
  1. Reload the page.
The admin panel.

Since the rule automatically changed role from "basic" to "admin" in the response before it reached the browser, the conditional check is satisfied and displayAdminUI() is executed, rendering the administrative panel in the user-interface.

Accessing the Additional Features

To access the additional user-interface features that are only intended for admin users:

  1. Copy featureFlags: [] from the response body.

NOTE

When copying values to match against from requests or responses, ensure to view their raw representation to ensure correct formatting.

  1. Create another rule in the Match & Replace interface that targets the Request Body.
  2. Select String from the Matcher drop-down menu and paste featureFlags: [] in the input field.
  3. In the Replacer input field, type in featureFlags: ['bouncy-ball', 'sparkle-background'].
  4. Click on the Intercept checkbox, the Update button, and the rule's associated checkbox in the Default Collection to add and enable the rule.
Creating a the feature flag rule.
  1. Reload the page.
The additional user-interface features.

In combination with the previous rule, since the rule automatically added the features to the array in the response before it reached the browser, when checkFeatureFlags() is executed, if (user.featureFlags.includes('bouncy-ball')) and if (user.featureFlags.includes('sparkle-background')) is satisfied and the createBouncyBall() and enableSparkleBackground() functions are executed.

Additional Modification

Rules can also be created to target entire blocks instead of just individual values or lines.

For example, by matching against the setTimeout function and replacing it with an empty string or comment, you can effectively remove the code from the page.

Matcher: String

txt
            // Fade out and remove
            setTimeout(() => {
                sparkle.style.opacity = '0';
                sparkle.style.transition = 'opacity 1s';
                setTimeout(() => {
                    document.body.removeChild(sparkle);
                }, 1000);
            }, 1000);

Replacer: String

txt
// Leave empty or add this comment
Creating a rule to persist the sparkles.

Now, once the page is reloaded, the sparkles will persist.

Permanent sparkles.