Skip to content

SSRF Prevention error downloading images.contentstack.io assets during export in CLI >= 1.46.1 #2727

Description

@spower-Personal

Description

Contentstack CLI export/backup fails when attempting to download certain assets from https://images.contentstack.io, reporting an SSRF prevention error.

The same backup pipeline and Contentstack content successfully export when using Contentstack CLI 1.38.0.

The issue occurs with both:

  • Contentstack CLI 1.46.1
  • Contentstack CLI 2.0.1

CLI 1.38.0 does not produce the SSRF error and successfully completes the asset download.

Based on a comparison of the packages included with the working and failing CLI versions, this appears to potentially be related to a change in the asset download implementation introduced after CLI 1.38.0, rather than an issue with Bash or basic network connectivity.

Error

The CLI reports an error similar to:

Error: SSRF Prevention: URL "https://images.contentstack.io/v3/assets/<redacted>/bltda70d33c86925cd4/6596d62c14e77e3cde117f02/Logo_(Dar...)" is not allowed

The rejected URL is hosted on Contentstack's own images.contentstack.io domain.

An example of the URL pattern being rejected is:

https://images.contentstack.io/v3/assets/<redacted>/bltda70d33c86925cd4/6596d62c14e77e3cde117f02/Logo_(Dar...)

Some portions of the URL have been redacted.

Environment

  • Contentstack CLI failing versions:
    • 1.46.1
    • 2.0.1
  • Contentstack CLI working version:
    • 1.38.0
  • Node.js:
    • 22.23.1
  • Execution environment:
    • Azure DevOps release pipeline
    • Bash shell

The same backup pipeline succeeds when the Contentstack CLI is downgraded to 1.38.0.

Version Comparison

Contentstack CLI 1.38.0

Contentstack CLI 1.38.0 includes:

@contentstack/cli-cm-export ~1.15.2
@contentstack/management ~1.18.4

Inspection of the distributed @contentstack/cli-cm-export 1.15.2 package shows that assets were downloaded directly through the package's HTTP client.

The relevant implementation resembles:

self.httpClient
    .options({ responseType: 'stream' })
    .get(self.assetStream.url)

In simplified terms, the download path appears to be:

CLI
  -> HTTP client
     -> https://images.contentstack.io/...
        -> asset download

This version successfully downloads the affected assets in our Azure DevOps backup pipeline.

Contentstack CLI 1.46.1

Contentstack CLI 1.46.1 includes:

@contentstack/cli-cm-export ~1.20.1
@contentstack/management ~1.22.0

Inspection of @contentstack/cli-cm-export 1.20.1 shows that the asset download implementation changed.

The export package now calls:

this.stack
    .asset()
    .download({ url, responseType: 'stream' })

The asset URL passed to this method is prepared using:

url: encodeURI(url)

This is invoked through the download-asset path in the export module.

In simplified terms, the newer download path appears to be:

CLI
  -> Contentstack Management SDK
     -> stack.asset().download()
        -> https://images.contentstack.io/...
           -> asset download

This is materially different from the direct HTTP GET used by the older export implementation.

Suspected Regression

The behavioral change appears to have been introduced somewhere between:

Contentstack CLI 1.38.0
  @contentstack/cli-cm-export 1.15.2
  @contentstack/management 1.18.4

and:

Contentstack CLI 1.46.1
  @contentstack/cli-cm-export 1.20.1
  @contentstack/management 1.22.0

The significant implementation difference we identified is the change from a direct HTTP asset download to:

stack.asset().download()

The behavior we are seeing is:

  1. CLI 1.38.0 succeeds.
  2. CLI 1.46.1 fails with the SSRF prevention error.
  3. CLI 2.0.1 also fails with the same SSRF prevention error.
  4. All versions are being used against the same Contentstack content and the same Azure DevOps backup pipeline.

This suggests that the SSRF rejection may be associated with the newer Management SDK-based asset download path.

We have not identified the exact commit that introduced the SSRF validation, so this should be considered a suspected regression based on the package implementation and observed behavior rather than a confirmed root cause.

Why the SSRF Rejection Appears Unexpected

The rejected URL is not an arbitrary external URL. It is an asset hosted on Contentstack's own image service:

https://images.contentstack.io/...

We therefore would not expect this URL to be rejected during a normal Contentstack stack export.

Possible areas that may be worth investigating include:

  • SSRF validation performed by stack.asset().download()
  • Host/domain validation or allowlisting for images.contentstack.io
  • DNS/IP validation performed as part of SSRF prevention
  • Redirect validation during asset download
  • URL parsing or normalization
  • Interaction between encodeURI() and the Management SDK
  • Asset filenames containing spaces, parentheses, or other special characters
  • Differences between the previous direct HTTP download implementation and the newer Management SDK download implementation

The particular URL currently producing the failure contains a filename beginning with:

Logo_(Dar...

Because of this, URL parsing or encoding differences may also be relevant, although we have not confirmed that the filename itself contributes to the SSRF rejection.

Operational Impact

We currently cannot identify a Contentstack CLI version that reliably handles both sides of our backup/restore workflow.

CLI 2.0.1

We need to use the newer CLI because it avoids import/restore problems encountered when using older CLI versions.

However, the backup/export process encounters the SSRF prevention error when downloading certain assets.

CLI 1.46.1

This version also encounters the same SSRF prevention error during backup/export.

CLI 1.38.0

Backup/export works with this version, including downloading the affected assets.

However, we have encountered other issues with the older CLI during restore/import, including what appear to be rate-limit-related failures where some content may not be restored successfully.

As a result, remaining on 1.38.0 is not a viable long-term workaround.

Effectively, we currently have:

CLI 1.38.0
  Backup/export: Works
  Restore/import: Problems

CLI 1.46.1
  Backup/export: SSRF failure

CLI 2.0.1
  Backup/export: SSRF failure
  Restore/import: Needed to avoid problems with older CLI

This prevents us from moving the backup/restore pipeline cleanly onto the current CLI release.

Expected Behavior

A Contentstack stack export should be able to download valid asset URLs hosted on:

https://images.contentstack.io

without triggering SSRF prevention.

If a Contentstack-hosted asset URL genuinely fails an SSRF security check, it would also be helpful for the CLI to report the specific reason the URL was classified as unsafe.

Actual Behavior

CLI 1.46.1 and 2.0.1 reject the Contentstack-hosted asset URL with:

SSRF Prevention: URL "<Contentstack asset URL>" is not allowed

CLI 1.38.0 successfully downloads the same asset when run through the same backup pipeline.

Reproduction Summary

At a high level:

  1. Run the Contentstack stack backup/export through the Azure DevOps pipeline using CLI 1.38.0.
  2. The affected images.contentstack.io asset downloads successfully.
  3. Run the same backup/export against the same stack using CLI 1.46.1.
  4. The asset download fails with the SSRF prevention error.
  5. Repeat using CLI 2.0.1.
  6. The same SSRF prevention error occurs.

No Contentstack content or pipeline configuration changes are required to reproduce the difference other than changing the Contentstack CLI version.

Questions for the Contentstack Team

Could the Contentstack team please confirm:

  1. Was SSRF validation added or changed in the asset download path between @contentstack/cli-cm-export 1.15.2 and 1.20.1?

  2. Does stack.asset().download() perform SSRF validation that was not performed by the previous direct HTTP download implementation?

  3. Should https://images.contentstack.io always be considered a valid asset download source by the Management SDK?

  4. Is the SSRF validation performed locally by the Management SDK/CLI, or is the URL submitted to a Contentstack API that performs the SSRF validation server-side?

  5. Can DNS resolution, IP validation, HTTP redirects, URL encoding, or particular filename characters cause a legitimate images.contentstack.io URL to fail the SSRF validation?

  6. Could the use of:

encodeURI(url)

before passing the URL to:

stack.asset().download()

affect how certain Contentstack asset URLs are validated?

  1. Is there additional debug logging we can enable that would show exactly why the URL is being classified as unsafe?

  2. Is there a supported workaround that would allow us to remain on Contentstack CLI 2.0.1 while avoiding this SSRF failure?

Additional Information

We can provide the complete affected asset URL privately if required for troubleshooting, as well as additional Azure DevOps pipeline logs.

We can also perform additional diagnostics against the affected asset URL if helpful, including:

  • DNS resolution from the Azure DevOps execution environment
  • Redirect-chain inspection
  • HTTP response/header inspection
  • Testing the asset URL directly with curl
  • Testing other assets to determine whether the problem is specific to certain filenames or URLs
  • Running the Contentstack CLI with additional debug/verbose logging
  • Comparing behavior using the same Node.js version and execution environment across different Contentstack CLI releases

The strongest indication of a regression is currently the reproducible version-dependent behavior:

Same stack
Same assets
Same Azure DevOps pipeline
Same general execution environment

CLI 1.38.0 -> asset download succeeds
CLI 1.46.1 -> SSRF Prevention error
CLI 2.0.1  -> SSRF Prevention error

Combined with the change in the export package from a direct HTTP download to stack.asset().download(), this appears to provide a relatively narrow area for investigation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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