Skip to content

Commit b302109

Browse files
Zacqarykibanamachinejs-jankisalviadcoelho
authored
[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)
## Summary Closes #205558 Updates the RRule library to correctly handle some scenarios with invalid parameters that would either cause it to return strange recurrence data or to infinitely loop. Specifically: - On `RRule` object creation, removes and ignores any `bymonth`, `bymonthday`, `byweekday`, or `byyearday` value that's out of bounds, e.g. less than 0 or greater than the number of possible months, days, weekdays, etc. - Successfully ignores cases of `BYMONTH=2, BYMONTHDAY=30` (February 30th), an input that's complicated to invalidate but still won't ever occur Allowing these values to go unhandled led to unpredictable behavior. The RRule library uses Moment.js to compare dates, but Moment.js months, days, and other values generally start at `0` while RRule values start at `1`. That led to several circumstances where we passed Moment.js a value of `-1`, which Moment.js interpreted as moving to the ***previous*** year, month, or other period of time. At worst, this could cause an infinite loop because the RRule library was constantly iterating through the wrong year, never reaching the date it was supposed to end on. In addition to making the RRule library more able to handle these cases, this PR also gives it a hard 100,000 iteration limit to prevent any possible infinite loops we've missed. Lastly, the Snooze Schedule APIs also come with additional validation to hopefully prevent out of bounds dates from ever being set. ### 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 --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Janki Salvi <[email protected]> Co-authored-by: Janki Salvi <[email protected]> Co-authored-by: adcoelho <[email protected]>
1 parent ca42d93 commit b302109

File tree

9 files changed

+615
-68
lines changed

9 files changed

+615
-68
lines changed

src/platform/packages/shared/kbn-rrule/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
export { RRule, Frequency, Weekday } from './rrule';
11-
export type { Options } from './rrule';
12-
export declare type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
10+
export { RRule } from './rrule';
11+
export type { Options, WeekdayStr } from './types';
12+
export { Frequency, Weekday } from './types';

src/platform/packages/shared/kbn-rrule/rrule.test.ts

Lines changed: 276 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
*/
99

1010
import sinon from 'sinon';
11-
import { RRule, Frequency, Weekday } from './rrule';
11+
import { RRule } from './rrule';
12+
import { Frequency, Weekday } from './types';
1213

1314
const DATE_2019 = '2019-01-01T00:00:00.000Z';
1415
const DATE_2019_DECEMBER_19 = '2019-12-19T00:00:00.000Z';
@@ -730,6 +731,228 @@ describe('RRule', () => {
730731
]
731732
`);
732733
});
734+
it('ignores invalid byweekday values', () => {
735+
const rule = new RRule({
736+
dtstart: new Date(DATE_2019_DECEMBER_19),
737+
freq: Frequency.WEEKLY,
738+
interval: 1,
739+
tzid: 'UTC',
740+
byweekday: [Weekday.TH, 0, -2],
741+
});
742+
expect(rule.all(14)).toMatchInlineSnapshot(`
743+
Array [
744+
2019-12-19T00:00:00.000Z,
745+
2019-12-26T00:00:00.000Z,
746+
2020-01-02T00:00:00.000Z,
747+
2020-01-09T00:00:00.000Z,
748+
2020-01-16T00:00:00.000Z,
749+
2020-01-23T00:00:00.000Z,
750+
2020-01-30T00:00:00.000Z,
751+
2020-02-06T00:00:00.000Z,
752+
2020-02-13T00:00:00.000Z,
753+
2020-02-20T00:00:00.000Z,
754+
2020-02-27T00:00:00.000Z,
755+
2020-03-05T00:00:00.000Z,
756+
2020-03-12T00:00:00.000Z,
757+
2020-03-19T00:00:00.000Z,
758+
]
759+
`);
760+
761+
const rule2 = new RRule({
762+
dtstart: new Date(DATE_2019),
763+
freq: Frequency.WEEKLY,
764+
interval: 1,
765+
tzid: 'UTC',
766+
byweekday: [Weekday.SA, Weekday.SU, Weekday.MO, 0],
767+
});
768+
769+
expect(rule2.all(9)).toMatchInlineSnapshot(`
770+
Array [
771+
2019-01-05T00:00:00.000Z,
772+
2019-01-06T00:00:00.000Z,
773+
2019-01-07T00:00:00.000Z,
774+
2019-01-12T00:00:00.000Z,
775+
2019-01-13T00:00:00.000Z,
776+
2019-01-14T00:00:00.000Z,
777+
2019-01-19T00:00:00.000Z,
778+
2019-01-20T00:00:00.000Z,
779+
2019-01-21T00:00:00.000Z,
780+
]
781+
`);
782+
});
783+
});
784+
785+
describe('bymonth', () => {
786+
it('works with yearly frequency', () => {
787+
const rule = new RRule({
788+
dtstart: new Date(DATE_2019_DECEMBER_19),
789+
freq: Frequency.YEARLY,
790+
interval: 1,
791+
tzid: 'UTC',
792+
bymonth: [2, 5],
793+
});
794+
expect(rule.all(14)).toMatchInlineSnapshot(`
795+
Array [
796+
2020-02-19T00:00:00.000Z,
797+
2020-05-19T00:00:00.000Z,
798+
2021-02-19T00:00:00.000Z,
799+
2021-05-19T00:00:00.000Z,
800+
2022-02-19T00:00:00.000Z,
801+
2022-05-19T00:00:00.000Z,
802+
2023-02-19T00:00:00.000Z,
803+
2023-05-19T00:00:00.000Z,
804+
2024-02-19T00:00:00.000Z,
805+
2024-05-19T00:00:00.000Z,
806+
2025-02-19T00:00:00.000Z,
807+
2025-05-19T00:00:00.000Z,
808+
2026-02-19T00:00:00.000Z,
809+
2026-05-19T00:00:00.000Z,
810+
]
811+
`);
812+
});
813+
it('ignores invalid bymonth values', () => {
814+
const rule = new RRule({
815+
dtstart: new Date(DATE_2019_DECEMBER_19),
816+
freq: Frequency.YEARLY,
817+
interval: 1,
818+
tzid: 'UTC',
819+
bymonth: [0],
820+
});
821+
expect(rule.all(14)).toMatchInlineSnapshot(`
822+
Array [
823+
2019-12-19T00:00:00.000Z,
824+
2020-12-19T00:00:00.000Z,
825+
2021-12-19T00:00:00.000Z,
826+
2022-12-19T00:00:00.000Z,
827+
2023-12-19T00:00:00.000Z,
828+
2024-12-19T00:00:00.000Z,
829+
2025-12-19T00:00:00.000Z,
830+
2026-12-19T00:00:00.000Z,
831+
2027-12-19T00:00:00.000Z,
832+
2028-12-19T00:00:00.000Z,
833+
2029-12-19T00:00:00.000Z,
834+
2030-12-19T00:00:00.000Z,
835+
2031-12-19T00:00:00.000Z,
836+
2032-12-19T00:00:00.000Z,
837+
]
838+
`);
839+
});
840+
});
841+
842+
describe('bymonthday', () => {
843+
it('works with monthly frequency', () => {
844+
const rule = new RRule({
845+
dtstart: new Date(DATE_2019_DECEMBER_19),
846+
freq: Frequency.MONTHLY,
847+
interval: 1,
848+
tzid: 'UTC',
849+
bymonthday: [1, 15],
850+
});
851+
expect(rule.all(14)).toMatchInlineSnapshot(`
852+
Array [
853+
2020-01-01T00:00:00.000Z,
854+
2020-01-15T00:00:00.000Z,
855+
2020-02-01T00:00:00.000Z,
856+
2020-02-15T00:00:00.000Z,
857+
2020-03-01T00:00:00.000Z,
858+
2020-03-15T00:00:00.000Z,
859+
2020-04-01T00:00:00.000Z,
860+
2020-04-15T00:00:00.000Z,
861+
2020-05-01T00:00:00.000Z,
862+
2020-05-15T00:00:00.000Z,
863+
2020-06-01T00:00:00.000Z,
864+
2020-06-15T00:00:00.000Z,
865+
2020-07-01T00:00:00.000Z,
866+
2020-07-15T00:00:00.000Z,
867+
]
868+
`);
869+
});
870+
it('ignores invalid bymonthday values', () => {
871+
const rule = new RRule({
872+
dtstart: new Date(DATE_2019_DECEMBER_19),
873+
freq: Frequency.MONTHLY,
874+
interval: 1,
875+
tzid: 'UTC',
876+
bymonthday: [0, -1, 32],
877+
});
878+
expect(rule.all(14)).toMatchInlineSnapshot(`
879+
Array [
880+
2019-12-19T00:00:00.000Z,
881+
2020-01-19T00:00:00.000Z,
882+
2020-02-19T00:00:00.000Z,
883+
2020-03-19T00:00:00.000Z,
884+
2020-04-19T00:00:00.000Z,
885+
2020-05-19T00:00:00.000Z,
886+
2020-06-19T00:00:00.000Z,
887+
2020-07-19T00:00:00.000Z,
888+
2020-08-19T00:00:00.000Z,
889+
2020-09-19T00:00:00.000Z,
890+
2020-10-19T00:00:00.000Z,
891+
2020-11-19T00:00:00.000Z,
892+
2020-12-19T00:00:00.000Z,
893+
2021-01-19T00:00:00.000Z,
894+
]
895+
`);
896+
});
897+
});
898+
899+
describe('bymonth, bymonthday', () => {
900+
it('works with yearly frequency', () => {
901+
const rule = new RRule({
902+
dtstart: new Date(DATE_2019_DECEMBER_19),
903+
freq: Frequency.YEARLY,
904+
interval: 1,
905+
tzid: 'UTC',
906+
bymonth: [2, 5],
907+
bymonthday: [8],
908+
});
909+
expect(rule.all(14)).toMatchInlineSnapshot(`
910+
Array [
911+
2020-02-08T00:00:00.000Z,
912+
2020-05-08T00:00:00.000Z,
913+
2021-02-08T00:00:00.000Z,
914+
2021-05-08T00:00:00.000Z,
915+
2022-02-08T00:00:00.000Z,
916+
2022-05-08T00:00:00.000Z,
917+
2023-02-08T00:00:00.000Z,
918+
2023-05-08T00:00:00.000Z,
919+
2024-02-08T00:00:00.000Z,
920+
2024-05-08T00:00:00.000Z,
921+
2025-02-08T00:00:00.000Z,
922+
2025-05-08T00:00:00.000Z,
923+
2026-02-08T00:00:00.000Z,
924+
2026-05-08T00:00:00.000Z,
925+
]
926+
`);
927+
});
928+
it('ignores valid dates that do not exist e.g. February 30th', () => {
929+
const rule = new RRule({
930+
dtstart: new Date(DATE_2019_DECEMBER_19),
931+
freq: Frequency.YEARLY,
932+
interval: 1,
933+
tzid: 'UTC',
934+
bymonth: [2, 5],
935+
bymonthday: [30],
936+
});
937+
expect(rule.all(14)).toMatchInlineSnapshot(`
938+
Array [
939+
2020-05-30T00:00:00.000Z,
940+
2021-05-30T00:00:00.000Z,
941+
2022-05-30T00:00:00.000Z,
942+
2023-05-30T00:00:00.000Z,
943+
2024-05-30T00:00:00.000Z,
944+
2025-05-30T00:00:00.000Z,
945+
2026-05-30T00:00:00.000Z,
946+
2027-05-30T00:00:00.000Z,
947+
2028-05-30T00:00:00.000Z,
948+
2029-05-30T00:00:00.000Z,
949+
2030-05-30T00:00:00.000Z,
950+
2031-05-30T00:00:00.000Z,
951+
2032-05-30T00:00:00.000Z,
952+
2033-05-30T00:00:00.000Z,
953+
]
954+
`);
955+
});
733956
});
734957

735958
describe('byhour, byminute, bysecond', () => {
@@ -844,6 +1067,30 @@ describe('RRule', () => {
8441067
]
8451068
`);
8461069
});
1070+
it('ignores invalid byyearday values', () => {
1071+
const rule = new RRule({
1072+
dtstart: new Date(DATE_2020),
1073+
freq: Frequency.YEARLY,
1074+
byyearday: [0, -1],
1075+
interval: 1,
1076+
tzid: 'UTC',
1077+
});
1078+
1079+
expect(rule.all(10)).toMatchInlineSnapshot(`
1080+
Array [
1081+
2020-01-01T00:00:00.000Z,
1082+
2021-01-01T00:00:00.000Z,
1083+
2022-01-01T00:00:00.000Z,
1084+
2023-01-01T00:00:00.000Z,
1085+
2024-01-01T00:00:00.000Z,
1086+
2025-01-01T00:00:00.000Z,
1087+
2026-01-01T00:00:00.000Z,
1088+
2027-01-01T00:00:00.000Z,
1089+
2028-01-01T00:00:00.000Z,
1090+
2029-01-01T00:00:00.000Z,
1091+
]
1092+
`);
1093+
});
8471094
});
8481095

8491096
describe('error handling', () => {
@@ -872,5 +1119,33 @@ describe('RRule', () => {
8721119
`"Cannot create RRule: until is an invalid date"`
8731120
);
8741121
});
1122+
1123+
it('throws an error on an interval of 0', () => {
1124+
const testFn = () =>
1125+
new RRule({
1126+
dtstart: new Date(DATE_2020),
1127+
freq: Frequency.HOURLY,
1128+
interval: 0,
1129+
tzid: 'UTC',
1130+
});
1131+
expect(testFn).toThrowErrorMatchingInlineSnapshot(
1132+
`"Cannot create RRule: interval must be greater than 0"`
1133+
);
1134+
});
1135+
1136+
it('throws an error when exceeding the iteration limit', () => {
1137+
const testFn = () => {
1138+
const rule = new RRule({
1139+
dtstart: new Date(DATE_2020),
1140+
freq: Frequency.YEARLY,
1141+
byyearday: [1],
1142+
interval: 1,
1143+
tzid: 'UTC',
1144+
});
1145+
rule.all(100001);
1146+
};
1147+
1148+
expect(testFn).toThrowErrorMatchingInlineSnapshot(`"RRule iteration limit exceeded"`);
1149+
});
8751150
});
8761151
});

0 commit comments

Comments
 (0)