cybersecurity · intermediate · ~15 min · safe pentest lab

CIDR Membership Check: ip_in_cidr

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.

Challenge

The problem

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 asset and the threat

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.

The task

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.

Example

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

Edge cases

  • /0 matches every address (zero bits compared).
  • /32 requires an exact 4-byte match.
  • Shifting a 32-bit value by 32 is undefined behaviour — handle the /0 mask without doing it.

Input format

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.

Output format

Return int 1 if ip is within net/prefix, otherwise 0. Out-of-range prefix returns 0.

Constraints

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.

Starter code

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;
}

Common mistakes

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).

Edge cases to handle

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.

Background lessons

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.