[FSH] @kbn/fs package #243037
Conversation
## 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>
77df488 to
2ffae5f
Compare
|
Pinging @elastic/kibana-security (Team:Security) |
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Public APIs missing exports
Page load bundle
History
|
|
Starting backport for target branches: 8.18, 8.19, 9.0, 9.1, 9.2 https://github.com/elastic/kibana/actions/runs/19624860785 |
💔 All backports failed
Manual backportTo create the backport manually run: Questions ?Please refer to the Backport tool documentation |
## 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
## 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
## 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
## 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
Note: Successful backport PRs will be merged automatically after passing CI. Manual backportTo create the backport manually run: Questions ?Please refer to the Backport tool documentation |
## 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
# 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-->
# 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-->
|
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. |
# 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-->
## 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>
Summary
Introduced the
@kbn/fspackage that wraps nodefsAPI with safe defaults:dataroot folderIntroduced eslint rule
@kbn/eslint/require_kbn_fsthat flags the direct writefscalls in production code. Severity is set towarnfor now and will be switched toerroronce migration is done in scope of #239385.Exposed interface has a
volumethat serves as logical namespace. You select a volume (e.g.,reports,exports/run-123) and pass a file name,getSafePathconstructs a full path underdata/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
Checklist
release_note:*label is applied per the guidelinesbackport:*labels.Closes: #239382