Skip to content

Commit 1944025

Browse files
[9.0] [Fleet] Improve file content type validation (elastic#234551) (elastic#234857)
# Backport This will backport the following commits from `main` to `9.0`: - [[Fleet] Improve file content type validation (elastic#234551)](elastic#234551) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nicolas Chaulet","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-09-12T06:54:00Z","message":"[Fleet] Improve file content type validation (elastic#234551)","sha":"0c5ab7ec467579bbe9c1feb5007efa811487db11","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","backport:all-open","v9.2.0"],"title":"[Fleet] Improve file content type validation","number":234551,"url":"https://github.com/elastic/kibana/pull/234551","mergeCommit":{"message":"[Fleet] Improve file content type validation (elastic#234551)","sha":"0c5ab7ec467579bbe9c1feb5007efa811487db11"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/234551","number":234551,"mergeCommit":{"message":"[Fleet] Improve file content type validation (elastic#234551)","sha":"0c5ab7ec467579bbe9c1feb5007efa811487db11"}}]}] BACKPORT--> Co-authored-by: Nicolas Chaulet <[email protected]>
1 parent abb21fc commit 1944025

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

x-pack/platform/plugins/shared/fleet/server/routes/epm/file_handler.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,31 @@ describe('getFileHandler', () => {
9696
);
9797
});
9898

99+
it('should throw if the file is not supported for bundled package and an existing file', async () => {
100+
mockedGetBundledPackageByPkgKey.mockResolvedValue({
101+
getBuffer: () => Promise.resolve(),
102+
} as any);
103+
const request = httpServerMock.createKibanaRequest({
104+
params: {
105+
pkgName: 'test',
106+
pkgVersion: '1.0.0',
107+
filePath: 'test.zip',
108+
},
109+
});
110+
const buffer = Buffer.from(`TEST`);
111+
mockedUnpackBufferEntries.mockResolvedValue([
112+
{
113+
path: 'test-1.0.0/test.zip',
114+
buffer,
115+
},
116+
]);
117+
const response = httpServerMock.createResponseFactory();
118+
const context = mockContext();
119+
await expect(getFileHandler(context, request, response)).rejects.toThrow(
120+
/File content type "application\/zip" is not allowed to be retrieved/
121+
);
122+
});
123+
99124
it('should a 404 for bundled package with a non existing file', async () => {
100125
mockedGetBundledPackageByPkgKey.mockResolvedValue({
101126
getBuffer: () => Promise.resolve(),
@@ -176,7 +201,7 @@ describe('getFileHandler', () => {
176201
body: 'not found',
177202
headers: new Headers({
178203
raw: '',
179-
'content-type': 'text',
204+
'content-type': 'text/plain',
180205
}),
181206
});
182207

@@ -187,7 +212,7 @@ describe('getFileHandler', () => {
187212
statusCode: 404,
188213
body: 'not found',
189214
headers: expect.objectContaining({
190-
'content-type': 'text',
215+
'content-type': 'text/plain',
191216
}),
192217
})
193218
);
@@ -227,6 +252,33 @@ describe('getFileHandler', () => {
227252
);
228253
});
229254

255+
it('should throw if the file is not a supported extension for installed package', async () => {
256+
const request = httpServerMock.createKibanaRequest({
257+
params: {
258+
pkgName: 'test',
259+
pkgVersion: '1.0.0',
260+
filePath: 'test.zip',
261+
},
262+
});
263+
const response = httpServerMock.createResponseFactory();
264+
const context = mockContext();
265+
266+
mockedGetInstallation.mockResolvedValue({ version: '1.0.0' } as any);
267+
mockedGetAsset.mockResolvedValue({
268+
asset_path: '/test/1.0.0/test.zip',
269+
data_utf8: 'test',
270+
data_base64: '',
271+
media_type: 'application/zip',
272+
package_name: 'test',
273+
package_version: '1.0.0',
274+
install_source: 'registry',
275+
});
276+
277+
await expect(getFileHandler(context, request, response)).rejects.toThrow(
278+
/File content type "application\/zip" is not allowed to be retrieved/
279+
);
280+
});
281+
230282
it('should a 404 if the file from installation do not exists for installed package', async () => {
231283
const request = httpServerMock.createKibanaRequest({
232284
params: {

x-pack/platform/plugins/shared/fleet/server/routes/epm/file_handler.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,30 @@ import { getAsset } from '../../services/epm/archive/storage';
1717
import { getBundledPackageByPkgKey } from '../../services/epm/packages/bundled_packages';
1818
import { pkgToPkgKey } from '../../services/epm/registry';
1919
import { unpackArchiveEntriesIntoMemory } from '../../services/epm/archive';
20+
import { FleetError } from '../../errors';
2021

2122
const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = {
2223
'cache-control': 'max-age=600',
2324
};
25+
26+
const ALLOWED_MIME_TYPES = [
27+
'image/svg+xml',
28+
'image/jpeg',
29+
'image/png',
30+
'image/gif',
31+
'application/json',
32+
'application/yaml',
33+
'text/plain',
34+
'text/markdown',
35+
'text/yaml',
36+
];
37+
38+
function validateContentTypeIsAllowed(contentType: string) {
39+
if (!ALLOWED_MIME_TYPES.includes(contentType.split(';')[0])) {
40+
throw new FleetError(`File content type "${contentType}" is not allowed to be retrieved`, 400);
41+
}
42+
}
43+
2444
export const getFileHandler: FleetRequestHandler<
2545
TypeOf<typeof GetFileRequestSchema.params>
2646
> = async (context, request, response) => {
@@ -42,6 +62,7 @@ export const getFileHandler: FleetRequestHandler<
4262
}
4363

4464
const contentType = storedAsset.media_type;
65+
validateContentTypeIsAllowed(contentType);
4566
const buffer = storedAsset.data_utf8
4667
? Buffer.from(storedAsset.data_utf8, 'utf8')
4768
: Buffer.from(storedAsset.data_base64, 'base64');
@@ -94,6 +115,7 @@ export const getFileHandler: FleetRequestHandler<
94115
statusCode: 400,
95116
});
96117
}
118+
validateContentTypeIsAllowed(contentType);
97119

98120
return response.custom({
99121
body: buffer,
@@ -121,6 +143,11 @@ export const getFileHandler: FleetRequestHandler<
121143
return headers;
122144
}, {} as ResponseHeaders);
123145

146+
if (!proxiedHeaders['content-type'] || typeof proxiedHeaders['content-type'] !== 'string') {
147+
throw new FleetError(`unknown content type for file: ${filePath}`);
148+
}
149+
validateContentTypeIsAllowed(proxiedHeaders['content-type']);
150+
124151
return response.custom({
125152
body: registryResponse.body,
126153
statusCode: registryResponse.status,

x-pack/platform/plugins/shared/fleet/server/routes/epm/handlers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ export const getInputsHandler: FleetRequestHandler<
578578
prerelease,
579579
ignoreUnverified
580580
);
581+
return response.ok({ body });
581582
} else if (format === 'yml' || format === 'yaml') {
582583
body = await getTemplateInputs(
583584
soClient,
@@ -587,8 +588,10 @@ export const getInputsHandler: FleetRequestHandler<
587588
prerelease,
588589
ignoreUnverified
589590
);
591+
592+
return response.ok({ body, headers: { 'content-type': 'text/yaml;charset=utf-8' } });
590593
}
591-
return response.ok({ body });
594+
throw new FleetError(`Fleet error template format not supported ${format}`);
592595
};
593596

594597
// Don't expose the whole SO in the API response, only selected fields

0 commit comments

Comments
 (0)