Path traversal vulnerability in gatsby-plugin-sharp
Description
Path traversal in gatsby-plugin-sharp allows reading arbitrary files when the Gatsby develop server is exposed to untrusted networks; patched in versions 5.8.1 and 4.25.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Path traversal in gatsby-plugin-sharp allows reading arbitrary files when the Gatsby develop server is exposed to untrusted networks; patched in versions 5.8.1 and 4.25.1.
Vulnerability
Overview
CVE-2023-30548 is a path traversal vulnerability in the gatsby-plugin-sharp plugin for the Gatsby framework. The plugin fails to properly validate file paths when serving static assets during development, allowing an attacker to traverse outside the project directory. This issue affects versions prior to 5.8.1 and 4.25.1 [1].
Exploitation
Conditions
By default, the Gatsby develop server (gatsby develop) listens only on localhost (127.0.0.1), making exploitation impossible from remote networks. However, if a developer explicitly exposes the server to other interfaces (e.g., using --host 0.0.0.0 or the GATSBY_HOST=0.0.0.0 environment variable), an attacker can send crafted HTTP requests containing path traversal sequences such as %2e%2e to read arbitrary files. The commit diffs [3][4] include a test that verifies the fix by attempting to access a file outside the project root.
Impact
Successful exploitation grants an attacker read access to all files within the scope of the server process. This could expose sensitive information such as source code, configuration files, environment variables, or other data stored on the server's filesystem [1].
Mitigation
The vulnerability is patched in gatsby-plugin-sharp@5.8.1 and gatsby-plugin-sharp@4.25.1. Users are strongly encouraged to upgrade. For those unable to upgrade immediately, the risk can be mitigated by ensuring the develop server is not exposed to untrusted networks; the default localhost-only configuration is safe [1].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
gatsby-plugin-sharpnpm | >= 5.0.0, < 5.8.1 | 5.8.1 |
gatsby-plugin-sharpnpm | < 4.25.1 | 4.25.1 |
Affected products
2Patches
2dcf88ed01df2fix(gatsby-plugin-sharp): don't serve static assets that are not result of currently triggered deferred job (#37796) (#37802)
7 files changed · +25 −8
e2e-tests/development-runtime/package.json+3 −2 modified@@ -32,14 +32,15 @@ "license": "MIT", "scripts": { "build": "gatsby build", - "develop": "cross-env CYPRESS_SUPPORT=y ENABLE_GATSBY_REFRESH_ENDPOINT=true GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND=y gatsby develop", + "develop": "cross-env CYPRESS_SUPPORT=y ENABLE_GATSBY_REFRESH_ENDPOINT=true GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND=y GATSBY_ENABLE_LAZY_IMAGES_IN_CI=y gatsby develop", "serve-static-files": "node ./serve-static-files.mjs", "serve": "gatsby serve", "clean": "gatsby clean", "typecheck": "tsc --noEmit", "start": "npm run develop", "format": "prettier --write \"src/**/*.js\"", "test": "npm run start-server-and-test || (npm run reset && exit 1)", + "test:dir-traversel-access": "! curl -f http://localhost:8000/%2e%2e/SHOULD_NOT_SERVE", "posttest": "npm run reset", "reset": "node scripts/reset.js", "reset:preview": "curl -X POST http://localhost:8000/__refresh", @@ -55,7 +56,7 @@ "playwright:debug": "playwright test --project=chromium --debug", "start-server-and-test:playwright": "start-server-and-test develop http://localhost:8000 serve-static-files http://localhost:8888 playwright", "start-server-and-test:playwright-debug": "start-server-and-test develop http://localhost:8000 serve-static-files http://localhost:8888 playwright:debug", - "combined": "npm run playwright && npm run cy:run", + "combined": "npm run playwright && npm run cy:run && npm run test:dir-traversel-access", "postinstall": "playwright install chromium" }, "devDependencies": {
e2e-tests/development-runtime/SHOULD_NOT_SERVE+1 −0 added@@ -0,0 +1 @@ +this file shouldn't be allowed to be served
e2e-tests/production-runtime/package.json+2 −1 modified@@ -36,6 +36,7 @@ "start": "npm run develop", "clean": "gatsby clean", "test": "npm run build && npm run start-server-and-test && npm run test-env-vars", + "test:dir-traversel-access": "! curl -f http://localhost:9000/%2e%2e/SHOULD_NOT_SERVE", "test:offline": "npm run build:offline && yarn start-server-and-test:offline && npm run test-env-vars", "test-env-vars": " node __tests__/env-vars.js", "start-server-and-test": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 combined", @@ -51,7 +52,7 @@ "playwright:debug": "playwright test --project=chromium --debug", "start-server-and-test:playwright": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 playwright", "start-server-and-test:playwright-debug": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 playwright:debug", - "combined": "npm run playwright && npm run cy:run", + "combined": "npm run playwright && npm run cy:run && npm run test:dir-traversel-access", "postinstall": "playwright install chromium" }, "devDependencies": {
e2e-tests/production-runtime/SHOULD_NOT_SERVE+1 −0 added@@ -0,0 +1 @@ +this file shouldn't be allowed to be served
packages/gatsby/cache-dir/__tests__/minimal-config.js+9 −0 modified@@ -25,6 +25,15 @@ it(`Builds cache-dir with minimal config`, done => { }) spawn.on(`close`, function () { + stderr = stderr + .replace(`Browserslist: caniuse-lite is outdated. Please run:`, ``) + .replace(`npx update-browserslist-db@latest`, ``) + .replace( + `Why you should do it regularly: https://github.com/browserslist/update-db#readme`, + `` + ) + .trim() + expect(stderr).toEqual(``) expect(stdout).not.toEqual(``) done()
packages/gatsby-plugin-sharp/src/gatsby-node.js+8 −4 modified@@ -33,16 +33,17 @@ exports.onCreateDevServer = async ({ app, cache, reporter }) => { const decodedURI = decodeURIComponent(req.path) const pathOnDisk = path.resolve(path.join(`./public/`, decodedURI)) - if (await pathExists(pathOnDisk)) { - return res.sendFile(pathOnDisk) - } - const jobContentDigest = await cache.get(decodedURI) const cacheResult = jobContentDigest ? await cache.get(jobContentDigest) : null if (!cacheResult) { + // this handler is meant to handle lazy images only (images that were registered for + // processing, but deffered to be processed only on request in develop server). + // If we don't have cache result - it means that this is not lazy image or that + // image was already handled in which case `express.static` handler (that is earlier + // than this handler) should take care of handling request. return next() } @@ -64,6 +65,9 @@ exports.onCreateDevServer = async ({ app, cache, reporter }) => { await removeCachedValue(cache, jobContentDigest) } + // we reach this point only when this is a lazy image that we just processed + // because `express.static` is earlier handler, we do have to manually serve + // produced file for current request return res.sendFile(pathOnDisk) }) }
packages/gatsby-plugin-sharp/src/index.js+1 −1 modified@@ -149,7 +149,7 @@ function createJob(job, { reporter }) { function lazyJobsEnabled() { return ( process.env.gatsby_executing_command === `develop` && - !isCI() && + (!isCI() || process.env.GATSBY_ENABLE_LAZY_IMAGES_IN_CI) && !( process.env.ENABLE_GATSBY_EXTERNAL_JOBS === `true` || process.env.ENABLE_GATSBY_EXTERNAL_JOBS === `1`
5f442081b227fix(gatsby-plugin-sharp): don't serve static assets that are not result of currently triggered deferred job (#37796) (#37799)
6 files changed · +16 −8
e2e-tests/development-runtime/package.json+3 −2 modified@@ -32,14 +32,15 @@ "license": "MIT", "scripts": { "build": "gatsby build", - "develop": "cross-env CYPRESS_SUPPORT=y ENABLE_GATSBY_REFRESH_ENDPOINT=y GATSBY_ENABLE_QUERY_ON_DEMAND_IN_CI=y gatsby develop", + "develop": "cross-env CYPRESS_SUPPORT=y ENABLE_GATSBY_REFRESH_ENDPOINT=y GATSBY_ENABLE_QUERY_ON_DEMAND_IN_CI=y GATSBY_ENABLE_LAZY_IMAGES_IN_CI=y gatsby develop", "serve-static-files": "node ./serve-static-files.mjs", "serve": "gatsby serve", "clean": "gatsby clean", "typecheck": "tsc --noEmit", "start": "npm run develop", "format": "prettier --write \"src/**/*.js\"", "test": "npm run start-server-and-test || (npm run reset && exit 1)", + "test:dir-traversel-access": "! curl -f http://localhost:8000/%2e%2e/SHOULD_NOT_SERVE", "posttest": "npm run reset", "reset": "node scripts/reset.js", "reset:preview": "curl -X POST http://localhost:8000/__refresh", @@ -55,7 +56,7 @@ "playwright:debug": "playwright test --project=chromium --debug", "start-server-and-test:playwright": "start-server-and-test develop http://localhost:8000 serve-static-files http://localhost:8888 playwright", "start-server-and-test:playwright-debug": "start-server-and-test develop http://localhost:8000 serve-static-files http://localhost:8888 playwright:debug", - "combined": "npm run playwright && npm run cy:run", + "combined": "npm run playwright && npm run cy:run && npm run test:dir-traversel-access", "postinstall": "playwright install chromium" }, "devDependencies": {
e2e-tests/development-runtime/SHOULD_NOT_SERVE+1 −0 added@@ -0,0 +1 @@ +this file shouldn't be allowed to be served
e2e-tests/production-runtime/package.json+2 −1 modified@@ -36,6 +36,7 @@ "start": "npm run develop", "clean": "gatsby clean", "test": "npm run build && npm run start-server-and-test && npm run test-env-vars", + "test:dir-traversel-access": "! curl -f http://localhost:9000/%2e%2e/SHOULD_NOT_SERVE", "test:offline": "npm run build:offline && yarn start-server-and-test:offline && npm run test-env-vars", "test-env-vars": " node __tests__/env-vars.js", "start-server-and-test": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 combined", @@ -51,7 +52,7 @@ "playwright:debug": "playwright test --project=chromium --debug", "start-server-and-test:playwright": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 playwright", "start-server-and-test:playwright-debug": "start-server-and-test serve http://localhost:9000 serve-static-files http://localhost:8888 playwright:debug", - "combined": "npm run playwright && npm run cy:run", + "combined": "npm run playwright && npm run cy:run && npm run test:dir-traversel-access", "postinstall": "playwright install chromium" }, "devDependencies": {
e2e-tests/production-runtime/SHOULD_NOT_SERVE+1 −0 added@@ -0,0 +1 @@ +this file shouldn't be allowed to be served
packages/gatsby-plugin-sharp/src/gatsby-node.js+8 −4 modified@@ -33,16 +33,17 @@ exports.onCreateDevServer = async ({ app, cache, reporter }) => { const decodedURI = decodeURIComponent(req.path) const pathOnDisk = path.resolve(path.join(`./public/`, decodedURI)) - if (await pathExists(pathOnDisk)) { - return res.sendFile(pathOnDisk) - } - const jobContentDigest = await cache.get(decodedURI) const cacheResult = jobContentDigest ? await cache.get(jobContentDigest) : null if (!cacheResult) { + // this handler is meant to handle lazy images only (images that were registered for + // processing, but deffered to be processed only on request in develop server). + // If we don't have cache result - it means that this is not lazy image or that + // image was already handled in which case `express.static` handler (that is earlier + // than this handler) should take care of handling request. return next() } @@ -64,6 +65,9 @@ exports.onCreateDevServer = async ({ app, cache, reporter }) => { await removeCachedValue(cache, jobContentDigest) } + // we reach this point only when this is a lazy image that we just processed + // because `express.static` is earlier handler, we do have to manually serve + // produced file for current request return res.sendFile(pathOnDisk) }) }
packages/gatsby-plugin-sharp/src/index.js+1 −1 modified@@ -149,7 +149,7 @@ function createJob(job, { reporter }) { function lazyJobsEnabled() { return ( process.env.gatsby_executing_command === `develop` && - !isCI() && + (!isCI() || process.env.GATSBY_ENABLE_LAZY_IMAGES_IN_CI) && !( process.env.ENABLE_GATSBY_EXTERNAL_JOBS === `true` || process.env.ENABLE_GATSBY_EXTERNAL_JOBS === `1`
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-h2pm-378c-pcxxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-30548ghsaADVISORY
- github.com/gatsbyjs/gatsby/commit/5f442081b227cc0879babb96858f970c4ce94c6bghsax_refsource_MISCWEB
- github.com/gatsbyjs/gatsby/commit/dcf88ed01df2c26e0c93a41e1a2a840076d8247eghsax_refsource_MISCWEB
- github.com/gatsbyjs/gatsby/security/advisories/GHSA-h2pm-378c-pcxxghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.