Skip to content

[Bug]: nginx/replace-env-vars.sh corrupts EXTENSIONS_API_URL with the API_URL value (unanchored sed), breaking Extensions → Explore #6809

Description

@sebasssti4n

Description of the issue

nginx/replace-env-vars.sh substitutes each config key in the built index.html with
an unanchored sed expression:

https://github.com/saleor/saleor-dashboard/blob/main/nginx/replace-env-vars.sh#L11-L27

# nginx/replace-env-vars.sh:11-20
replace_env_var() {
  var_name=$1
  var_value=$(eval echo \$"$var_name")
  if [ -n "$var_value" ]; then
    echo "Setting $var_name to: $var_value"
    sed -i "s#\([[:space:]]*\)$var_name:[[:space:]]*\"[^\"]*\"#\1$var_name: \"$var_value\"#" "$INDEX_BUNDLE_PATH"
  else
    echo "No $var_name provided, using defaults."
  fi
}

# lines 23-27
replace_env_var "API_URL"
replace_env_var "APP_MOUNT_URI"
replace_env_var "EXTENSIONS_API_URL"
replace_env_var "IS_CLOUD_INSTANCE"
replace_env_var "LOCALE_CODE"

The pattern begins with \([[:space:]]*\), which matches the empty string. It is
therefore not anchored to the start of a line, and API_URL: also matches as a
substring of EXTENSIONS_API_URL:. When replace_env_var "API_URL" runs (line 23),
it rewrites both lines of window.__SALEOR_CONFIG__:

        API_URL: "https://example.com/graphql/",
        EXTENSIONS_API_URL: "https://example.com/graphql/",   <-- collateral damage

replace_env_var "EXTENSIONS_API_URL" runs later (line 25) and would repair the
damage — but only if EXTENSIONS_API_URL is non-empty. On a self-hosted deployment
that intentionally leaves it empty (the supported configuration since #6391, which
falls back to the bundled extensions.json), the script takes the else branch and
the corrupted value survives.

The log output is misleading, because it reports intent rather than result:

Setting API_URL to: https://example.com/graphql/
Setting APP_MOUNT_URI to: /dashboard/
No EXTENSIONS_API_URL provided, using defaults.

…while the served index.html contains
EXTENSIONS_API_URL: "https://example.com/graphql/".

Downstream impact: Extensions → Explore throws for every staff user

useAppStoreExtensions reads that value and derives its fallback flag from whether it
is empty:

https://github.com/saleor/saleor-dashboard/blob/main/src/extensions/views/ExploreExtensions/hooks/useAppStoreExtensions.ts

// useAppStoreExtensions.ts:25-51
const fetchApiExtensions = async (url: string) => {
  const response = await fetch(url);
  if (!response.ok) throw new Error(response.statusText);
  const data = await response.json();          // line 32
  ...
};

const isFallback = !appStoreUrl;               // line 42
const categories = appStoreUrl
  ? await fetchApiExtensions(appStoreUrl)      // line 50
  : await loadFallbackExtensions();            // line 51

With the corrupted value the chain is:

  1. appStoreUrl is the GraphQL endpoint, so isFallback is false and the bundled
    catalogue is never used;
  2. fetch() on that URL returns 200 with an HTML body (the GraphQL playground),
    so response.ok is true and the !response.ok guard does not fire;
  3. response.json() throws a SyntaxError, which is caught and stored in error;
  4. ExploreExtensions rethrows it as a render-time error
    (ExploreExtensions.tsx:23-26if (error) { throw new Error(error); }),
    so the whole view is replaced by the generic error page.

Result: Extensions → Explore is unusable for every staff user on any self-hosted
deployment that leaves EXTENSIONS_API_URL empty — which is exactly the configuration
the static fallback was added for.

Steps to reproduce the problem

Self-contained reproduction (no Docker needed — the function below is copied verbatim
from nginx/replace-env-vars.sh):

#!/bin/sh
set -e
cd "$(mktemp -d)"

# index.html as emitted by the build from src/index.html
cat > index.html <<'EOF'
<!doctype html>
<html>
  <head>
    <script>
      window.__SALEOR_CONFIG__ = {
        API_URL: "",
        APP_MOUNT_URI: "/dashboard/",
        STATIC_URL: "/dashboard/",
        EXTENSIONS_API_URL: "",
        IS_CLOUD_INSTANCE: "",
        SALEOR_CLOUD_APP_DOMAIN: "",
        LOCALE_CODE: "EN",
      };
    </script>
  </head>
  <body><div id="dashboard-app"></div></body>
</html>
EOF

# verbatim from nginx/replace-env-vars.sh (line 16)
replace_env_var() {
  var_name=$1
  var_value=$(eval echo \$"$var_name")
  if [ -n "$var_value" ]; then
    echo "Setting $var_name to: $var_value"
    sed -i "s#\([[:space:]]*\)$var_name:[[:space:]]*\"[^\"]*\"#\1$var_name: \"$var_value\"#" index.html
  else
    echo "No $var_name provided, using defaults."
  fi
}

# typical self-hosted deployment: EXTENSIONS_API_URL deliberately empty
API_URL="https://example.com/graphql/"
APP_MOUNT_URI="/dashboard/"
EXTENSIONS_API_URL=""
IS_CLOUD_INSTANCE=""
LOCALE_CODE="EN"

for v in API_URL APP_MOUNT_URI EXTENSIONS_API_URL IS_CLOUD_INSTANCE LOCALE_CODE; do
  replace_env_var "$v"
done

echo "--- result ---"
grep -n 'API_URL' index.html

Output:

Setting API_URL to: https://example.com/graphql/
Setting APP_MOUNT_URI to: /dashboard/
No EXTENSIONS_API_URL provided, using defaults.
No IS_CLOUD_INSTANCE provided, using defaults.
Setting LOCALE_CODE to: EN
--- result ---
6:        API_URL: "https://example.com/graphql/",
9:        EXTENSIONS_API_URL: "https://example.com/graphql/",

End-to-end, in a container built from the repo Dockerfile:

  1. Run the Dashboard image with API_URL set and EXTENSIONS_API_URL unset/empty.
  2. curl the served /index.html and read window.__SALEOR_CONFIG__
    EXTENSIONS_API_URL equals API_URL.
  3. Sign in and open Extensions → Explore — the view renders the generic error page.

What did you expect to happen?

replace_env_var "API_URL" should only rewrite the API_URL entry. When
EXTENSIONS_API_URL is unset, it should remain "" and useAppStoreExtensions should
take the isFallback path and render the bundled catalogue with the self-hosted
banner.

Additional information

Proposed fix

Anchor the pattern to the start of the line. window.__SALEOR_CONFIG__ is emitted one
key per line by src/index.html, so ^ is sufficient and minimal:

--- a/nginx/replace-env-vars.sh
+++ b/nginx/replace-env-vars.sh
@@ -13,7 +13,7 @@ replace_env_var() {
   var_value=$(eval echo \$"$var_name")
   if [ -n "$var_value" ]; then
     echo "Setting $var_name to: $var_value"
-    sed -i "s#\([[:space:]]*\)$var_name:[[:space:]]*\"[^\"]*\"#\1$var_name: \"$var_value\"#" "$INDEX_BUNDLE_PATH"
+    sed -i "s#^\([[:space:]]*\)$var_name:[[:space:]]*\"[^\"]*\"#\1$var_name: \"$var_value\"#" "$INDEX_BUNDLE_PATH"
   else
     echo "No $var_name provided, using defaults."
   fi

Verified against the reproduction above with both GNU sed 4.9 and BusyBox sed 1.36.1
(the runtime image is nginx:stable-alpine, i.e. BusyBox):

  • EXTENSIONS_API_URL empty → stays "", API_URL still set correctly;
  • EXTENSIONS_API_URL set → still replaced correctly (no regression on the
    Saleor Cloud path).

If relying on line-per-key formatting feels too fragile, an equivalent alternative is
to require a boundary before the key, e.g. matching { or a line start explicitly, or
generating the config block from the environment instead of patching it in place.

Related hardening (optional, separate concern)

fetchApiExtensions treats any 2xx as valid JSON
(useAppStoreExtensions.ts:25-36). Checking Content-Type, or falling back to the
bundled catalogue when parsing fails instead of rethrowing into the error page, would
turn a misconfiguration into a degraded-but-usable screen rather than a hard failure.
That would also have made this bug far easier to diagnose.

Notes for triage

  • Not reproducible when EXTENSIONS_API_URL is set (the later call repairs the value),
    which is presumably why it has not surfaced on Saleor Cloud.
  • API_URL is currently the only key that is a proper suffix of another key in the
    replace list, so it is the only one that collides today — but the pattern would
    collide again for any future *_API_URL-style key.

Environment

Browser and version: N/A (container entrypoint / served index.html)
OS and version: Linux, Docker image based on nginx:stable-alpine (BusyBox sed 1.36.1)
Dashboard version: 3.23.16 (self-hosted); script unchanged on main @ 3.23.22
Core version: Saleor 3.23

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions