Skip to content

[FSH] @kbn/fs package #243037

Merged
elena-shostak merged 20 commits intoelastic:mainfrom
elena-shostak:feature/fsh
Nov 24, 2025
Merged

[FSH] @kbn/fs package #243037
elena-shostak merged 20 commits intoelastic:mainfrom
elena-shostak:feature/fsh

Conversation

@elena-shostak
Copy link
Copy Markdown
Contributor

@elena-shostak elena-shostak commented Nov 14, 2025

Summary

Introduced the @kbn/fs package that wraps node fs API with safe defaults:

  • every write resolves paths under the repo data root folder
  • validates that there is no path traversal
  • validates allowed file extensions
  • validates allowed mime types
  • validates allowed file sizes
  • performs sanitization for svg

Introduced eslint rule @kbn/eslint/require_kbn_fs that flags the direct write fs calls in production code. Severity is set to warn for now and will be switched to error once migration is done in scope of #239385.

Exposed interface has a volume that serves as logical namespace. You select a volume (e.g., reports, exports/run-123) and pass a file name, getSafePath constructs a full path under data/volume/.... That allows us to introduce per-volume restrictions later (file size, mime types, etc) if needed and extend the interface without friction.

How to test

You can add a test route to check that out easily.

POST /internal/security/files
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;
  
        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });
  
              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });
  
              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });
  
            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });
  
            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });
  
            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }

Checklist

  • Unit or functional tests were updated or added to match the most common scenarios
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.

Closes: #239382

@elena-shostak elena-shostak changed the title @kbn/fs package [FSH] @kbn/fs package Nov 14, 2025
@elena-shostak elena-shostak added Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// release_note:skip Skip the PR/issue when compiling release notes backport:version Backport to applied version labels v9.0.9 v9.2.2 v9.1.8 v8.19.8 v8.18.9 Feature:Hardening Harding of Kibana from a security perspective labels Nov 14, 2025
elena-shostak and others added 8 commits November 14, 2025 16:32
## Summary

Validation is covering the following scenarios:
- [x] validate that directory is a safe path
- [x] validate allowed file extensions
- [x] validate allowed mime types
- [x] validate allowed file sizes
- [x] perform sanitization of file content for svg


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

__Closes: https://github.com/elastic/kibana/issues/239383__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
@elena-shostak elena-shostak marked this pull request as ready for review November 17, 2025 13:29
@elena-shostak elena-shostak requested review from a team as code owners November 17, 2025 13:29
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/kibana-security (Team:Security)

@kibanamachine
Copy link
Copy Markdown
Contributor

kibanamachine commented Nov 17, 2025

Dependency Review Bot Analysis 🔍

Found 2 new third-party dependencies:

Package Version Vulnerabilities Health Score
magic-bytes.js ^1.12.1 🔴 C: 0, 🟠 H: 0, 🟡 M: 0, 🟢 L: 0 magic-bytes.js
dompurify ^3.3.0 🔴 C: 0, 🟠 H: 0, 🟡 M: 0, 🟢 L: 0 dompurify

Self Checklist

To help with the review, please update the PR description to address the following points for each new third-party dependency listed above:

  • Purpose: What is this dependency used for? Briefly explain its role in your changes.
  • Justification: Why is adding this dependency the best approach?
  • Alternatives explored: Were other options considered (e.g., using existing internal libraries/utilities, implementing the functionality directly)? If so, why was this dependency chosen over them?
  • Existing dependencies: Does Kibana have a dependency providing similar functionality? If so, why is the new one preferred?

Thank you for providing this information!

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/fs - 2 +2

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/fs - 3 +3

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
kbnUiSharedDeps-npmDll 6.4MB 6.4MB +3.0B
Unknown metric groups

API count

id before after diff
@kbn/fs - 38 +38

History

@elena-shostak elena-shostak merged commit 20efb62 into elastic:main Nov 24, 2025
13 checks passed
@kibanamachine
Copy link
Copy Markdown
Contributor

Starting backport for target branches: 8.18, 8.19, 9.0, 9.1, 9.2

https://github.com/elastic/kibana/actions/runs/19624860785

@kibanamachine
Copy link
Copy Markdown
Contributor

💔 All backports failed

Status Branch Result
8.18 Backport failed because of merge conflicts
8.19 Backport failed because of merge conflicts
9.0 Backport failed because of merge conflicts
9.1 Backport failed because of merge conflicts

You might need to backport the following PRs to 9.1:
- [CI] Auto regenerate moon projects (#237609)
- Update dependency @xyflow/react to ^12.9.2 (main) (#243136)
- Use a package-based TSConfig base file (#217159)
9.2 Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

node scripts/backport --pr 243037

Questions ?

Please refer to the Backport tool documentation

elena-shostak added a commit to elena-shostak/kibana that referenced this pull request Nov 24, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>

```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;

        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });

              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });

              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });

            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });

            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });

            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 20efb62)

# Conflicts:
#	.buildkite/scripts/steps/security/third_party_packages.txt
elena-shostak added a commit to elena-shostak/kibana that referenced this pull request Nov 24, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>

```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;

        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });

              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });

              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });

            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });

            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });

            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 20efb62)

# Conflicts:
#	.buildkite/scripts/steps/security/third_party_packages.txt
#	.eslintrc.js
#	.github/CODEOWNERS
#	package.json
#	src/dev/license_checker/config.ts
#	tsconfig.base.json
#	yarn.lock
elena-shostak added a commit to elena-shostak/kibana that referenced this pull request Nov 24, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>

```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;

        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });

              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });

              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });

            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });

            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });

            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 20efb62)

# Conflicts:
#	.buildkite/scripts/steps/security/third_party_packages.txt
#	.eslintrc.js
#	.github/CODEOWNERS
#	package.json
elena-shostak added a commit to elena-shostak/kibana that referenced this pull request Nov 24, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>

```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;

        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });

              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });

              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });

            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });

            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });

            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 20efb62)

# Conflicts:
#	.buildkite/scripts/steps/security/third_party_packages.txt
#	.eslintrc.js
#	.github/CODEOWNERS
#	package.json
#	packages/kbn-eslint-plugin-eslint/index.js
#	src/dev/license_checker/config.ts
#	tsconfig.base.json
#	yarn.lock
@elena-shostak
Copy link
Copy Markdown
Contributor Author

elena-shostak commented Nov 24, 2025

Status Branch Result
9.2
9.1
8.19

Note: Successful backport PRs will be merged automatically after passing CI.

Manual backport

To create the backport manually run:

node scripts/backport --pr 243037

Questions ?

Please refer to the Backport tool documentation

elena-shostak added a commit to elena-shostak/kibana that referenced this pull request Nov 24, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>

```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;

        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });

              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });

              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });

            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });

            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });

            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 20efb62)

# Conflicts:
#	.buildkite/scripts/steps/security/third_party_packages.txt
#	package.json
elena-shostak added a commit that referenced this pull request Nov 24, 2025
# Backport

This will backport the following commits from `main` to `9.2`:
- [[FSH] @kbn/fs package
(#243037)](#243037)

<!--- Backport version: 10.2.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Elena
Shostak","email":"165678770+elena-shostak@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-11-24T06:00:34Z","message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","Feature:Hardening","backport:version","v9.3.0","v8.18.9","v9.0.9","v8.19.8","v9.2.2","v9.1.8"],"title":"[FSH]
@kbn/fs package
","number":243037,"url":"https://github.com/elastic/kibana/pull/243037","mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},"sourceBranch":"main","suggestedTargetBranches":["8.18","9.0","8.19","9.2","9.1"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243037","number":243037,"mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},{"branch":"8.18","label":"v8.18.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
elena-shostak added a commit that referenced this pull request Nov 24, 2025
# Backport

This will backport the following commits from `main` to `8.19`:
- [[FSH] @kbn/fs package
(#243037)](#243037)

<!--- Backport version: 10.2.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Elena
Shostak","email":"165678770+elena-shostak@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-11-24T06:00:34Z","message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","Feature:Hardening","backport:version","v9.3.0","v8.18.9","v9.0.9","v8.19.8","v9.2.2","v9.1.8"],"title":"[FSH]
@kbn/fs package
","number":243037,"url":"https://github.com/elastic/kibana/pull/243037","mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},"sourceBranch":"main","suggestedTargetBranches":["8.18","9.0","8.19","9.2","9.1"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243037","number":243037,"mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},{"branch":"8.18","label":"v8.18.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Nov 25, 2025
@kibanamachine
Copy link
Copy Markdown
Contributor

Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
cc: @elena-shostak

elena-shostak added a commit that referenced this pull request Nov 25, 2025
# Backport

This will backport the following commits from `main` to `9.1`:
- [[FSH] @kbn/fs package
(#243037)](#243037)

<!--- Backport version: 10.2.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Elena
Shostak","email":"165678770+elena-shostak@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-11-24T06:00:34Z","message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","Feature:Hardening","backport:version","v9.3.0","v8.18.9","v9.0.9","v8.19.8","v9.2.2","v9.1.8"],"title":"[FSH]
@kbn/fs package
","number":243037,"url":"https://github.com/elastic/kibana/pull/243037","mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},"sourceBranch":"main","suggestedTargetBranches":["8.18","9.0","8.19","9.2","9.1"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243037","number":243037,"mergeCommit":{"message":"[FSH]
@kbn/fs package (#243037)\n\n## Summary\n\nIntroduced the `@kbn/fs`
package that wraps node `fs` API with safe\ndefaults:\n\n- every write
resolves paths under the repo `data` root folder\n- validates that there
is no path traversal\n- validates allowed file extensions\n- validates
allowed mime types\n- validates allowed file sizes\n- performs
sanitization for svg\n\nIntroduced eslint rule
`@kbn/eslint/require_kbn_fs` that flags the\ndirect write `fs` calls in
production code. Severity is set to `warn`\nfor now and will be switched
to `error` once migration is done in scope\nof
https://github.com/elastic/kibana/issues/239385.\n\nExposed interface
has a `volume` that serves as logical namespace. You\nselect a volume
(e.g., `reports`, `exports/run-123`) and pass a file\nname,
`getSafePath` constructs a full path under `data/volume/...`.
That\nallows us to introduce per-volume restrictions later (file size,
mime\ntypes, etc) if needed and extend the interface without
friction.\n\n## How to test\nYou can add a test route to check that out
easily.\n<details>\n <summary>POST /internal/security/files</summary>\n
\n```ts\n export function defineFileRoutes({ router }:
RouteDefinitionParams) {\n router.post(\n {\n path:
'/internal/security/files',\n security: {\n authz: {\n enabled: false,\n
reason: 'Test route for file operations',\n },\n },\n validate: {\n
request: {\n body: schema.object({\n method: schema.string(),\n name:
schema.string(),\n content: schema.string(),\n volume:
schema.maybe(schema.string()),\n }),\n },\n },\n },\n
createLicensedRouteHandler(async (context, request, response) => {\n
const { method, name, content, volume = 'security-test' } =
request.body;\n \n try {\n switch (method) {\n case 'writeFile':\n const
resultWriteFile = await writeFile(name, content, {\n volume,\n });\n \n
return response.ok({ body: resultWriteFile });\n case 'appendFile':\n
const resultAppendFile = await appendFile(name, content, {\n volume,\n
});\n \n return response.ok({ body: resultAppendFile });\n case
'writeFileSync':\n const resultWriteFileSync = writeFileSync(name,
content, {\n volume,\n });\n return response.ok({ body:
resultWriteFileSync });\n case 'appendFileSync':\n const
resultAppendFileSync = appendFileSync(name, content, {\n volume,\n });\n
return response.ok({ body: resultAppendFileSync });\n \n case
'createWriteStream':\n const writeStream = createWriteStream(name,
volume);\n writeStream.write(content);\n writeStream.end();\n return
response.ok({ body: 'ok' });\n \n case 'readFile':\n const fileContent =
await readFile(name, volume);\n return response.ok({ body: fileContent
});\n \n default:\n return
response.customError(wrapIntoCustomErrorResponse(new Error('Invalid
method')));\n }\n } catch (error) {\n return
response.customError(wrapIntoCustomErrorResponse(error));\n }\n })\n
);\n }\n```\n</details>\n\n### Checklist\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n-
[x] Review the
[backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand
apply applicable `backport:*` labels.\n\n\n\n__Closes:
https://github.com/elastic/kibana/issues/239382__\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"20efb621a082aae5320d62ce59a600d7a5884114"}},{"branch":"8.18","label":"v8.18.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.9","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
@kibanamachine kibanamachine removed the backport missing Added to PRs automatically when the are determined to be missing a backport. label Nov 25, 2025
eokoneyo pushed a commit to eokoneyo/kibana that referenced this pull request Dec 2, 2025
## Summary

Introduced the `@kbn/fs` package that wraps node `fs` API with safe
defaults:

- every write resolves paths under the repo `data` root folder
- validates that there is no path traversal
- validates allowed file extensions
- validates allowed mime types
- validates allowed file sizes
- performs sanitization for svg

Introduced eslint rule `@kbn/eslint/require_kbn_fs` that flags the
direct write `fs` calls in production code. Severity is set to `warn`
for now and will be switched to `error` once migration is done in scope
of elastic#239385.

Exposed interface has a `volume` that serves as logical namespace. You
select a volume (e.g., `reports`, `exports/run-123`) and pass a file
name, `getSafePath` constructs a full path under `data/volume/...`. That
allows us to introduce per-volume restrictions later (file size, mime
types, etc) if needed and extend the interface without friction.

## How to test
You can add a test route to check that out easily.
<details>
  <summary>POST /internal/security/files</summary>
  
```ts
  export function defineFileRoutes({ router }: RouteDefinitionParams) {
    router.post(
      {
        path: '/internal/security/files',
        security: {
          authz: {
            enabled: false,
            reason: 'Test route for file operations',
          },
        },
        validate: {
          request: {
            body: schema.object({
              method: schema.string(),
              name: schema.string(),
              content: schema.string(),
              volume: schema.maybe(schema.string()),
            }),
          },
        },
      },
      createLicensedRouteHandler(async (context, request, response) => {
        const { method, name, content, volume = 'security-test' } = request.body;
  
        try {
          switch (method) {
            case 'writeFile':
              const resultWriteFile = await writeFile(name, content, {
                volume,
              });
  
              return response.ok({ body: resultWriteFile });
            case 'appendFile':
              const resultAppendFile = await appendFile(name, content, {
                volume,
              });
  
              return response.ok({ body: resultAppendFile });
            case 'writeFileSync':
              const resultWriteFileSync = writeFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultWriteFileSync });
            case 'appendFileSync':
              const resultAppendFileSync = appendFileSync(name, content, {
                volume,
              });
              return response.ok({ body: resultAppendFileSync });
  
            case 'createWriteStream':
              const writeStream = createWriteStream(name, volume);
              writeStream.write(content);
              writeStream.end();
              return response.ok({ body: 'ok' });
  
            case 'readFile':
              const fileContent = await readFile(name, volume);
              return response.ok({ body: fileContent });
  
            default:
              return response.customError(wrapIntoCustomErrorResponse(new Error('Invalid method')));
          }
        } catch (error) {
          return response.customError(wrapIntoCustomErrorResponse(error));
        }
      })
    );
  }
```
</details>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [x] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.



__Closes: https://github.com/elastic/kibana/issues/239382__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:version Backport to applied version labels Feature:Hardening Harding of Kibana from a security perspective release_note:skip Skip the PR/issue when compiling release notes Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// v8.19.8 v9.1.8 v9.2.2 v9.3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce @kbn/fs package to handle file system operations

5 participants