cybersecurity · intermediate · ~15 min · safe pentest lab
Build a correct, deny-by-default CIDR membership test: derive a netmask from a prefix length, validate attacker-controlled bounds before use, and avoid the undefined-behaviour trap of an out-of-range bit shift.
IP allowlists and firewall ACLs are only as trustworthy as the CIDR-matching code behind them. If your membership check mishandles the prefix length or the netmask, an attacker's address can slip past an allow rule (or a legitimate host can be wrongly blocked). This lab has you write the core primitive that decides whether an address belongs to a network range.
The harness bakes a fixed network (192.168.1.0/24 and a few other ranges) into a local buffer. There is no live socket, no target host, and no filesystem access — you only compute over the bytes handed to you. The threat modeled here is an attacker-controlled prefix length: a caller may pass a negative prefix, 33, or something absurd like 999. Treat any out-of-range prefix as invalid and deny by default (return 0). Never let a bad prefix drive an undefined bit shift.
Implement int ip_in_cidr(const unsigned char ip[4], const unsigned char net[4], int prefix). Return 1 if and only if ip falls inside net/prefix by comparing only the top prefix bits (build a netmask, mask both addresses, compare). prefix is valid in 0..32; anything outside that range returns 0. Pure bit logic — no allocation, no I/O.
ip_in_cidr(inside, net, 24) -> 1 // inside is 192.168.1.55, net is 192.168.1.0
ip_in_cidr(outside, net, 24) -> 0 // outside is 192.168.2.1
ip_in_cidr(inside, net, 33) -> 0 // invalid prefix, rejected
/0 matches every address (zero bits compared)./32 requires an exact 4-byte match./0 mask without doing it.Three arguments: ip (4 unsigned bytes, big-endian order), net (4 unsigned bytes), and an int prefix. Buffers are exactly 4 bytes; prefix may be any int value including out-of-range.
Return int 1 if ip is within net/prefix, otherwise 0. Out-of-range prefix returns 0.
C11. No dynamic allocation, no I/O, no system calls. Read only the 4 bytes of each array. Do not perform a shift by 32 or more bits on a 32-bit type (undefined behaviour). Constant time relative to input is fine; only bit operations are needed.
int ip_in_cidr(const unsigned char ip[4], const unsigned char net[4], int prefix){
/* TODO: compare the top `prefix` bits of ip and net using a netmask.
Validate prefix is in 0..32 first; reject anything else. */
(void)ip; (void)net; (void)prefix;
return -1;
}Computing the mask as (0xFFFFFFFF << (32 - prefix)) when prefix == 0 shifts by 32, which is undefined behaviour and often yields the wrong mask. Forgetting to validate the prefix range and letting a negative or >32 prefix produce a garbage mask. Comparing raw bytes instead of masked values. Assembling the 32-bit address without casting bytes to unsigned int first, causing sign-extension or overflow. Returning the boolean of an unmasked equality (matching the full address instead of only the prefix bits).
prefix = 0 must match any address (mask is all zeros). prefix = 32 must require an exact match of all four bytes. prefix < 0 or prefix > 32 (e.g. -1, 33, 999) must return 0. The broadcast/last address in a range (e.g. 192.168.1.255 in /24) is still inside the range. Bytes at 255 must not be sign-extended incorrectly.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.