Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add IPv4 CIDR and IPv6 CIDR validations #367

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions library/src/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,26 @@ export const IPV4_REGEX =
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
/^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}$/u;

/**
* [IPv4 CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks)
*/
export const IPV4_CIDR_REGEX =
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
/^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}\/(?:\d|[12]\d|3[0-2])$/u;

/**
* [IPv6](https://en.wikipedia.org/wiki/IPv6) regex.
*/
export const IPV6_REGEX =
/^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu;

/**
* [IPv6 CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv6_CIDR_blocks)
*/
export const IPV6_CIDR_REGEX =
// eslint-disable-next-line redos-detector/no-unsafe-regex, regexp/no-super-linear-backtracking -- false positive
/^s*((([\dA-Fa-f]{1,4}:){7}([\dA-Fa-f]{1,4}|:))|(([\dA-Fa-f]{1,4}:){6}(:[\dA-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\dA-Fa-f]{1,4}:){5}(((:[\dA-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\dA-Fa-f]{1,4}:){4}(((:[\dA-Fa-f]{1,4}){1,3})|((:[\dA-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\dA-Fa-f]{1,4}:){3}(((:[\dA-Fa-f]{1,4}){1,4})|((:[\dA-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\dA-Fa-f]{1,4}:){2}(((:[\dA-Fa-f]{1,4}){1,5})|((:[\dA-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\dA-Fa-f]{1,4}:)(((:[\dA-Fa-f]{1,4}){1,6})|((:[\dA-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[\dA-Fa-f]{1,4}){1,7})|((:[A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)*(\/(\d|[1-9]\d|1[01]\d|12[0-8]))$/u;
Copy link
Contributor

@kurtextrem kurtextrem Jan 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand why the s* is needed here. What does it do?

Additionally, are we sure that both redos and no-super-linear-backtracking are false positives?

I haven't had the time to try https://2bdenny.github.io/ReScue/ & https://github.com/doyensec/regexploit, but if one of them shows a vulnerability too, we have 3 online and at least one offline tool showing it's vulnerable, which tells us it's certainly vulnerable.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into the vulnerabilities further. I’m not sure I understood them correctly, thanks for pointing this out.

Copy link
Contributor

@ariskemper ariskemper Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kurtextrem thanks for the review, i agree with you. We should not use Regex which doesn't pass those regex exploits checking tools.


/**
* [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date regex.
*/
Expand Down
2 changes: 2 additions & 0 deletions library/src/validations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export * from './includes/index.ts';
export * from './integer/integer.ts';
export * from './ip/index.ts';
export * from './ipv4/index.ts';
export * from './ipv4Cidr/index.ts';
export * from './ipv6/index.ts';
export * from './ipv6Cidr/index.ts';
export * from './isoDate/index.ts';
export * from './isoDateTime/index.ts';
export * from './isoTime/index.ts';
Expand Down
1 change: 1 addition & 0 deletions library/src/validations/ipv4Cidr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ipv4Cidr.ts';
33 changes: 33 additions & 0 deletions library/src/validations/ipv4Cidr/ipv4Cidr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from 'vitest';
import { ipv4Cidr } from './ipv4Cidr.ts';

describe('ipv4cidr', () => {
test('should pass only IP v4 CIDR notation', () => {
const validate = ipv4Cidr();
const value1 = '192.168.1.1/24';
expect(validate._parse(value1).output).toBe(value1);
const value2 = '127.0.0.1/16';
expect(validate._parse(value2).output).toBe(value2);
const value3 = '0.0.0.0/0';
expect(validate._parse(value3).output).toBe(value3);
const value4 = '255.255.255.255/32';
expect(validate._parse(value4).output).toBe(value4);

expect(validate._parse('').issues).toBeTruthy();
expect(validate._parse('1').issues).toBeTruthy();
expect(validate._parse('-1.0.0.0').issues).toBeTruthy();
expect(validate._parse('0..0.0.0').issues).toBeTruthy();
expect(validate._parse('1234.0.0.0').issues).toBeTruthy();
expect(validate._parse('256.256.256.256').issues).toBeTruthy();
expect(validate._parse('1.2.3').issues).toBeTruthy();
expect(validate._parse('192.168.1.1/01').issues).toBeTruthy();
expect(validate._parse('192.168.1.1/33').issues).toBeTruthy();
expect(validate._parse('192.168.1.1/-1').issues).toBeTruthy();
});

test('should return custom error message', () => {
const error = 'Value is not in IP v4 CIDR notation!';
const validate = ipv4Cidr(error);
expect(validate._parse('test').issues?.[0].message).toBe(error);
});
});
40 changes: 40 additions & 0 deletions library/src/validations/ipv4Cidr/ipv4Cidr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IPV4_CIDR_REGEX } from '../../regex.ts';
import type { BaseValidation, ErrorMessage } from '../../types/index.ts';
import { actionIssue, actionOutput } from '../../utils/index.ts';

/**
* IPv4 CIDR validation type.
*/
export type Ipv4CidrValidation<TInput extends string> = BaseValidation<TInput> & {
/**
* The validation type.
*/
type: 'ipv4cidr';
/**
* The IPv4 CIDR regex.
*/
requirement: RegExp;
};

/**
* Creates a pipeline validation action that validates a [IPv4 CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks) notation.
*
* @param message The error message.
*
* @returns A validation action.
*/
export function ipv4Cidr<TInput extends string>(
message: ErrorMessage = 'Invalid IPv4 CIDR'
): Ipv4CidrValidation<TInput> {
return {
type: 'ipv4cidr',
async: false,
message,
requirement: IPV4_CIDR_REGEX,
_parse(input) {
return !this.requirement.test(input)
? actionIssue(this.type, this.message, input, this.requirement)
: actionOutput(input);
},
};
}
1 change: 1 addition & 0 deletions library/src/validations/ipv6Cidr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ipv6Cidr.ts';
36 changes: 36 additions & 0 deletions library/src/validations/ipv6Cidr/ipv6Cidr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test } from 'vitest';
import { ipv6Cidr } from './ipv6Cidr.ts';

describe('ipv6', () => {
test('should pass only IP v6 CIDR notation', () => {
const validate = ipv6Cidr();
const value1 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334/0';
expect(validate._parse(value1).output).toBe(value1);
const value2 = 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329/64';
expect(validate._parse(value2).output).toBe(value2);
const value3 = 'fe80::1ff:fe23:4567:890a/128';
expect(validate._parse(value3).output).toBe(value3);
const value4 = '2001:db8:85a3:8d3:1319:8a2e:370:7348/48';
expect(validate._parse(value4).output).toBe(value4);

expect(validate._parse('').issues).toBeTruthy();
expect(validate._parse('1').issues).toBeTruthy();
expect(validate._parse('1.2.3').issues).toBeTruthy();
expect(validate._parse('192.168.1.1').issues).toBeTruthy();
expect(validate._parse('0.0.0.0').issues).toBeTruthy();
expect(
validate._parse('test:test:test:test:test:test:test:test').issues
).toBeTruthy();
expect(validate._parse('2001:db8:85a3:8d3:1319:8a2e:370:7348/129').issues).toBeTruthy()
expect(validate._parse('2001:db8:85a3:8d3:1319:8a2e:370:7348/01').issues).toBeTruthy()
expect(validate._parse('2001:db8:85a3:8d3:1319:8a2e:370:7348').issues).toBeTruthy()
expect(validate._parse('2001:db8:85a3:8d3:1319:8a2e:370:7348/-1').issues).toBeTruthy()
expect(validate._parse('2001:db8:85a3:8d3:1319:8a2e:370:7348/test').issues).toBeTruthy()
});

test('should return custom error message', () => {
const error = 'Value is not in IP v6 CIDR notation!';
const validate = ipv6Cidr(error);
expect(validate._parse('test').issues?.[0].message).toBe(error);
});
});
40 changes: 40 additions & 0 deletions library/src/validations/ipv6Cidr/ipv6Cidr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IPV6_CIDR_REGEX } from '../../regex.ts';
import type { BaseValidation, ErrorMessage } from '../../types/index.ts';
import { actionIssue, actionOutput } from '../../utils/index.ts';

/**
* IPv6 CIDR validation type.
*/
export type Ipv6CidrValidation<TInput extends string> = BaseValidation<TInput> & {
/**
* The validation type.
*/
type: 'ipv6cidr';
/**
* The IPv6 CIDR regex.
*/
requirement: RegExp;
};

/**
* Creates a pipeline validation action that validates a [IPv6 CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv6_CIDR_blocks) notation.
*
* @param message The error message.
*
* @returns A validation action.
*/
export function ipv6Cidr<TInput extends string>(
message: ErrorMessage = 'Invalid IPv6 CIDR'
): Ipv6CidrValidation<TInput> {
return {
type: 'ipv6cidr',
async: false,
message,
requirement: IPV6_CIDR_REGEX,
_parse(input) {
return !this.requirement.test(input)
? actionIssue(this.type, this.message, input, this.requirement)
: actionOutput(input);
},
};
}