[Security Solution][Endpoint] Validate path values for trusted apps (#99035)

* Validate path values for trusted apps

show soft warnings when path values are not valid.

refs elastic/security-team/issues/315

* use case insensitive flag

refs 71ac9bdeaf

* correct check for windows paths

review changes

* rename

review changes

* add validations to include ? for wildcards

also add more tests
refs elastic/security-team/issues/315

* update copy for soft errors

refs elastic/security-team/issues/315

* refactor validation logic

review changes

refs elastic/kibana/pull/99035#discussion_r625106658

* allow wildcards in path names

refs elastic/security-team/issues/315

* stack soft errors

refs elastic/security-team/issues/315

* Update x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx

Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>

* remove links to private repos

review changes

* improve windows path regex

refactor tests for better debugging
review changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
This commit is contained in:
Ashokaditya 2021-05-10 16:18:48 +02:00 committed by GitHub
parent 9715157467
commit da890fd24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 621 additions and 6 deletions

View file

@ -0,0 +1,506 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isPathValid } from './validations';
import { OperatingSystem, ConditionEntryField } from '../../types';
describe('Unacceptable Windows wildcard paths', () => {
it('should not accept paths that do not have a folder name with a wildcard ', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:\\folder',
})
).toEqual(false);
});
it('should not accept paths that do not have a file name with a wildcard ', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:\\path.exe',
})
).toEqual(false);
});
it('should not accept nested paths that do not have a wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:\\folder\\path.exe',
})
).toEqual(false);
});
it('should not accept paths with * wildcard and /', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:/**/path.exe',
})
).toEqual(false);
});
it('should not accept paths with ? wildcard and /', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:/?indows/pat?',
})
).toEqual(false);
});
});
describe('Acceptable Windows wildcard paths', () => {
it('should accept wildcards for folders', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:\\**\\path.exe',
})
).toEqual(true);
});
it('should accept wildcards for folders and files', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'e:\\**\\*.exe',
})
).toEqual(true);
});
it('should accept paths with single wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'f:\\*',
})
).toEqual(true);
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'f:\\?',
})
).toEqual(true);
});
it('should accept paths that have wildcard in filenames', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'a:\\*.*',
})
).toEqual(true);
});
it('should accept paths with ? as wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:\\?indows\\pat?',
})
).toEqual(true);
});
it('should accept paths with both ? and * as wildcards', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:\\*?',
})
).toEqual(true);
});
it('should accept paths with multiple wildcards', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:\\**',
})
).toEqual(true);
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:\\??',
})
).toEqual(true);
});
});
describe('Acceptable Windows exact paths', () => {
it('should accept paths when it ends with a folder name', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\folder',
})
).toEqual(true);
});
it('should accept paths when it ends with a file name', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\path.exe',
})
).toEqual(true);
});
it('should accept paths when it ends with a filename in a folder', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\folder\\path.exe',
})
).toEqual(true);
});
});
describe('Acceptable Windows exact paths with hyphens', () => {
it('should accept paths when paths have folder names with hyphens', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\hype-folder-name',
})
).toEqual(true);
});
it('should accept paths when file names have hyphens', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\file-name.exe',
})
).toEqual(true);
});
});
describe('Unacceptable Windows exact paths', () => {
it('should not accept paths with /', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:/folder/path.exe',
})
).toEqual(false);
});
it('should not accept paths not having a <char:> in the suffix', () => {
expect(
isPathValid({
os: OperatingSystem.WINDOWS,
field: ConditionEntryField.PATH,
type: 'match',
value: '\\folder\\path.exe',
})
).toEqual(false);
});
});
///
describe('Unacceptable Mac/Linux wildcard paths', () => {
it('should not accept paths that do not have a folder name with a wildcard ', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/folder',
})
).toEqual(false);
});
it('should not accept paths that do not have a file name with a wildcard ', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/zip.zip',
})
).toEqual(false);
});
it('should not accept nested paths that do not have a wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/opt/pack.tar',
})
).toEqual(false);
});
it('should not accept paths with * wildcard and \\', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'c:\\**\\path.exe',
})
).toEqual(false);
});
it('should not accept paths with ? wildcard and \\', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: 'C:\\?indows\\pat?',
})
).toEqual(false);
});
});
describe('Acceptable Mac/Linux wildcard paths', () => {
it('should accept wildcards for folders', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/**/file.',
})
).toEqual(true);
});
it('should accept wildcards for folders and files', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/usr/bi?/*.js',
})
).toEqual(true);
});
it('should accept paths with single wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/op*',
})
).toEqual(true);
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/op?',
})
).toEqual(true);
});
it('should accept paths that have wildcard in filenames', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/*.*',
})
).toEqual(true);
});
it('should accept paths with ? as wildcard', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/usr/?inux/pat?',
})
).toEqual(true);
});
it('should accept paths with both ? and * as wildcards', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/usr/*?',
})
).toEqual(true);
});
it('should accept paths with multiple wildcards', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/usr/**',
})
).toEqual(true);
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'wildcard',
value: '/opt/??',
})
).toEqual(true);
});
});
describe('Acceptable Mac/Linux exact paths', () => {
it('should accept paths when it is the root path', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'match',
value: '/',
})
).toEqual(true);
});
it('should accept paths when it ends with a file name', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'match',
value: '/usr/file.ts',
})
).toEqual(true);
});
it('should accept paths when it ends with a filename in a folder', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'match',
value: '/opt/z.dmg',
})
).toEqual(true);
});
});
describe('Acceptable Mac/Linux exact paths with hyphens', () => {
it('should accept paths when paths have folder names with hyphens', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'match',
value: '/hype-folder-name',
})
).toEqual(true);
});
it('should accept paths when file names have hyphens', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'match',
value: '/file-name.dmg',
})
).toEqual(true);
});
});
describe('Unacceptable Mac/Linux exact paths', () => {
it('should not accept paths with \\', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'match',
value: 'c:\\folder\\path.exe',
})
).toEqual(false);
});
it('should not accept paths not starting with /', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'match',
value: 'opt/bin',
})
).toEqual(false);
});
it('should not accept paths ending with /', () => {
expect(
isPathValid({
os: OperatingSystem.MAC,
field: ConditionEntryField.PATH,
type: 'match',
value: '/opt/bin/',
})
).toEqual(false);
});
it('should not accept file extensions with hyphens', () => {
expect(
isPathValid({
os: OperatingSystem.LINUX,
field: ConditionEntryField.PATH,
type: 'match',
value: '/opt/bin/file.d-mg',
})
).toEqual(false);
});
});

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { ConditionEntry, ConditionEntryField } from '../../types';
import {
ConditionEntry,
ConditionEntryField,
OperatingSystem,
TrustedAppEntryTypes,
} from '../../types';
const HASH_LENGTHS: readonly number[] = [
32, // MD5
@ -28,3 +33,88 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => {
.filter((entry) => entry[1].length > 1)
.map((entry) => entry[0]);
};
export const isPathValid = ({
os,
field,
type,
value,
}: {
os: OperatingSystem;
field: ConditionEntryField;
type: TrustedAppEntryTypes;
value: string;
}): boolean => {
if (field === ConditionEntryField.PATH) {
if (type === 'wildcard') {
return os === OperatingSystem.WINDOWS
? isWindowsWildcardPathValid(value)
: isLinuxMacWildcardPathValid(value);
}
return doesPathMatchRegex({ value, os });
}
return true;
};
const doesPathMatchRegex = ({ os, value }: { os: OperatingSystem; value: string }): boolean => {
if (os === OperatingSystem.WINDOWS) {
const filePathRegex = /^[a-z]:(?:|\\\\[^<>:"'/\\|?*]+\\[^<>:"'/\\|?*]+|%\w+%|)[\\](?:[^<>:"'/\\|?*]+[\\/])*([^<>:"'/\\|?*])+$/i;
return filePathRegex.test(value);
}
return /^(\/|(\/[\w\-]+)+|\/[\w\-]+\.[\w]+|(\/[\w-]+)+\/[\w\-]+\.[\w]+)$/i.test(value);
};
const isWindowsWildcardPathValid = (path: string): boolean => {
const firstCharacter = path[0];
const lastCharacter = path.slice(-1);
const trimmedValue = path.trim();
const hasSlash = /\//.test(trimmedValue);
if (path.length === 0) {
return false;
} else if (
hasSlash ||
trimmedValue.length !== path.length ||
firstCharacter === '^' ||
lastCharacter === '\\' ||
!hasWildcard({ path, isWindowsPath: true })
) {
return false;
} else {
return true;
}
};
const isLinuxMacWildcardPathValid = (path: string): boolean => {
const firstCharacter = path[0];
const lastCharacter = path.slice(-1);
const trimmedValue = path.trim();
if (path.length === 0) {
return false;
} else if (
trimmedValue.length !== path.length ||
firstCharacter !== '/' ||
lastCharacter === '/' ||
path.length > 1024 === true ||
path.includes('//') === true ||
!hasWildcard({ path, isWindowsPath: false })
) {
return false;
} else {
return true;
}
};
const hasWildcard = ({
path,
isWindowsPath,
}: {
path: string;
isWindowsPath: boolean;
}): boolean => {
for (const pathComponent of path.split(isWindowsPath ? '\\' : '/')) {
if (/[\*|\?]+/.test(pathComponent) === true) {
return true;
}
}
return false;
};

View file

@ -25,7 +25,10 @@ import {
NewTrustedApp,
OperatingSystem,
} from '../../../../../../common/endpoint/types';
import { isValidHash } from '../../../../../../common/endpoint/service/trusted_apps/validations';
import {
isValidHash,
isPathValid,
} from '../../../../../../common/endpoint/service/trusted_apps/validations';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import {
@ -53,8 +56,8 @@ const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
interface FieldValidationState {
/** If this fields state is invalid. Drives display of errors on the UI */
isInvalid: boolean;
errors: string[];
warnings: string[];
errors: React.ReactNode[];
warnings: React.ReactNode[];
}
interface ValidationResult {
/** Overall indicator if form is valid */
@ -72,7 +75,7 @@ const addResultToValidation = (
validation: ValidationResult,
field: keyof NewTrustedApp,
type: 'warnings' | 'errors',
resultValue: string
resultValue: React.ReactNode
) => {
if (!validation.result[field]) {
validation.result[field] = {
@ -81,7 +84,8 @@ const addResultToValidation = (
warnings: [],
};
}
validation.result[field]![type].push(resultValue);
const errorMarkup: React.ReactNode = type === 'warnings' ? <div>{resultValue}</div> : resultValue;
validation.result[field]![type].push(errorMarkup);
validation.result[field]!.isInvalid = true;
};
@ -154,6 +158,20 @@ const validateFormValues = (values: MaybeImmutable<NewTrustedApp>): ValidationRe
values: { row: index + 1 },
})
);
} else if (
!isPathValid({ os: values.os, field: entry.field, type: entry.type, value: entry.value })
) {
// show soft warnings and thus allow entry
isValid = true;
addResultToValidation(
validation,
'entries',
'warnings',
i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidPathMsg', {
defaultMessage: '[{row}] Path may be formed incorrectly; verify value',
values: { row: index + 1 },
})
);
}
});
}
@ -468,6 +486,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
data-test-subj={getTestId('conditionsRow')}
isInvalid={wasVisited?.entries && validationResult.result.entries?.isInvalid}
error={validationResult.result.entries?.errors}
helpText={validationResult.result.entries?.warnings}
>
<LogicalConditionBuilder
entries={trustedApp.entries}