Setiap data yang masuk dari luar—baik dari pengguna, file konfigurasi, jaringan, atau argumen command-line—harus diperlakukan sebagai tidak tepercaya sampai terbukti sah. Prinsip input validation at boundary menyatakan bahwa kita memvalidasi data tepat di titik masuk program, bukan di dalam fungsi internal yang mungkin menerima data yang sudah diproses sebagian. Pada chapter ini kita membangun sebuah validator umur yang menggabungkan tiga lapis pemeriksaan: panjang string, tipe karakter, dan rentang numerik. Kombinasi tiga lapis inilah yang membedakan validasi kokoh dari pemeriksaan permukaan yang mudah ditembus.
Tiga Lapisan Validasi di Boundary #
Aturan praktisnya sederhana: tolak lebih awal, terima lebih lambat. Bila data gagal di lapisan pertama, program tidak boleh melanjutkan ke lapisan berikutnya. Pendekatan ini menghemat sumber daya dan mengurangi permukaan serangan. Mari kita lihat implementasinya:
/* input_validation.c — validasi panjang, tipe, dan range pada boundary. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>
#define MAX_INPUT 32
#define MIN_AGE 0
#define MAX_AGE 150
/* Cek panjang string pada boundary. */
static int check_length(const char *s, size_t max_len) {
if (s == NULL) return 0;
size_t len = strlen(s);
if (len == 0 || len > max_len) {
fprintf(stderr, "[REJECT] panjang %zu di luar [1,%zu]\n", len, max_len);
return 0;
}
return 1;
}
/* Cek tipe: hanya digit, opsional diawali tanda minus/plus. */
static int check_type_integer(const char *s) {
if (s == NULL || *s == '\0') return 0;
size_t i = 0;
if (s[0] == '+' || s[0] == '-') i = 1;
if (s[i] == '\0') return 0; /* hanya tanda, tanpa digit */
for (; s[i] != '\0'; i++) {
if (!isdigit((unsigned char)s[i])) {
fprintf(stderr, "[REJECT] karakter non-digit di posisi %zu\n", i);
return 0;
}
}
return 1;
}
/* Cek range numerik dengan strtol yang aman. */
static int parse_and_check_range(const char *s, long min_val, long max_val, long *out) {
errno = 0;
char *endp = NULL;
long v = strtol(s, &endp, 10);
if (errno == ERANGE) {
fprintf(stderr, "[REJECT] nilai di luar jangkauan long\n");
return 0;
}
if (v < min_val || v > max_val) {
fprintf(stderr, "[REJECT] nilai %ld di luar [%ld,%ld]\n", v, min_val, max_val);
return 0;
}
*out = v;
return 1;
}
/* Fungsi boundary yang menggabungkan tiga lapisan validasi. */
static int validate_age(const char *input, long *age_out) {
if (!check_length(input, MAX_INPUT)) return 0;
if (!check_type_integer(input)) return 0;
if (!parse_and_check_range(input, MIN_AGE, MAX_AGE, age_out)) return 0;
return 1;
}
int main(void) {
/* Jaga urutan output: stdout tidak di-buffer supaya selaras dengan stderr. */
setvbuf(stdout, NULL, _IONBF, 0);
const char *inputs[] = {
"25",
"180",
"abc",
"",
"99999999999999999999",
"-5",
"30"
};
const size_t n = sizeof(inputs) / sizeof(inputs[0]);
for (size_t i = 0; i < n; i++) {
printf("--- Input ke-%zu: '%s' ---\n", i + 1, inputs[i]);
long age = -1;
if (validate_age(inputs[i], &age)) {
printf("[ACCEPT] umur valid = %ld\n", age);
}
}
return 0;
}Output yang Dihasilkan:
--- Input ke-1: '25' ---
[ACCEPT] umur valid = 25
--- Input ke-2: '180' ---
[REJECT] nilai 180 di luar [0,150]
--- Input ke-3: 'abc' ---
[REJECT] karakter non-digit di posisi 0
--- Input ke-4: '' ---
[REJECT] panjang 0 di luar [1,32]
--- Input ke-5: '99999999999999999999' ---
[REJECT] nilai di luar jangkauan long
--- Input ke-6: '-5' ---
[REJECT] nilai -5 di luar [0,150]
--- Input ke-7: '30' ---
[ACCEPT] umur valid = 30Mari kita telaah baris-baris kode di atas:
- Baris 12-19 (
check_length): Lapisan pertama. Fungsi ini menolak pointerNULL, string kosong, dan string yang lebih panjang dariMAX_INPUT(32 karakter). Membatasi panjang sejak awal mencegah denial of service akibat input sangat panjang yang membebani parser atau alokator. - Baris 22-33 (
check_type_integer): Lapisan kedua memeriksa apakah setiap karakter sesuai dengan format integer. Penggunaanisdigit((unsigned char)c)penting karenacharbertanda dapat menyebabkan undefined behavior bila di-castkeintlangsung pada nilai negatif. - Baris 36-49 (
parse_and_check_range): Lapisan ketiga memakaistrtolyang lebih aman daripadaatoi. Fungsiatoitidak melaporkan overflow maupun kesalahan format—atoi("99999999999999999999")akan mengembalikan nilai tak terdefinisi.strtolmengaturerrno = ERANGEbila hasil di luar bataslong, danendpmenunjukkan di mana parsing berhenti. - Baris 52-57 (
validate_age): Fungsi boundary ini menggabungkan tiga lapisan secara berurutan dan kembali0segera setelah salah satu lapisan gagal. Pola short-circuit ini menjaga agar lapisan berikutnya tidak pernah memproses data yang sudah ditolak. - Baris 60 (
setvbuf): Karenastderrtidak di-buffer sementarastdoutdi-buffer, pesan[REJECT]dan--- Input ke-N ---dapat muncul tidak berurutan pada pipa. Mematikan buffer stdout menjamin urutan tampil sesuai eksekusi.
Jebakan atoi() dan Mengapa strtol Lebih Aman #
atoi terlihat lebih ringkas, tetapi tidak memberi sinyal kesalahan apapun. atoi("abc") mengembalikan 0—nilai valid untuk MIN_AGE. atoi("99999999999999999999") memicu undefined behavior karena hasil tidak muat di int. strtol menyelesaikan kedua masalah dengan errno dan parameter endp yang menunjukkan karakter pertama yang tidak diproses.
Contoh Tambahan: Validasi Email #
Pola tiga lapisan dapat diterapkan pada format data lain. Berikut validator email sederhana yang memeriksa panjang, struktur (keberadaan @ dan . di domain), serta karakter yang diizinkan:
/* extra_input_validation.c — validasi email sederhana. */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static int is_valid_email(const char *s) {
if (s == NULL || *s == '\0') return 0;
size_t len = strlen(s);
if (len > 254) return 0; /* RFC 5321 batas panjang */
const char *at = strchr(s, '@');
if (at == NULL || at == s || at == s + len - 1) return 0;
if (strchr(at + 1, '@') != NULL) return 0; /* hanya satu @ */
/* Local part tidak boleh kosong, hanya alfanumerik dan . _ % + - */
for (const char *p = s; p < at; p++) {
if (!(isalnum((unsigned char)*p) || *p == '.' || *p == '_'
|| *p == '%' || *p == '+' || *p == '-')) return 0;
}
/* Domain harus punya setidaknya satu titik setelah @ */
const char *dot = strchr(at + 1, '.');
if (dot == NULL || dot == at + 1 || dot == s + len - 1) return 0;
return 1;
}
int main(void) {
const char *tests[] = {
"[email protected]",
"[email protected]",
"tidak-valid",
"@nodomain.com",
"[email protected]",
"user@example",
"user@@double.com",
"[email protected]",
"[email protected]",
};
const size_t n = sizeof(tests) / sizeof(tests[0]);
for (size_t i = 0; i < n; i++) {
printf("[%s] '%s'\n",
is_valid_email(tests[i]) ? "VALID " : "INVALID",
tests[i]);
}
return 0;
}Output yang Dihasilkan:
[VALID ] '[email protected]'
[VALID ] '[email protected]'
[INVALID] 'tidak-valid'
[INVALID] '@nodomain.com'
[INVALID] '[email protected]'
[INVALID] 'user@example'
[INVALID] 'user@@double.com'
[VALID ] '[email protected]'
[VALID ] '[email protected]'Validator email produksi sebaiknya memakai pustaka khusus seperti libidn atau regex RFC 5322 yang lengkap—demo ini menangkap kasus umum tetapi tidak mengikuti seluruh aturan RFC.
Daftar Cek Validasi untuk Setiap Boundary #
Saat mendesain boundary baru, gunakan daftar cek berikut:
- Sumber: Apakah data berasal dari pengguna, file, jaringan, atau environment? Semua sumber eksternal dianggap tidak tepercaya.
- Panjang: Apakah ada batas atas eksplisit? Batasi panjang sebelum parsing untuk mencegah DoS.
- Tipe: Apakah format data cocok dengan ekspektasi? Misalnya integer tidak boleh mengandung huruf atau spasi.
- Range: Apakah nilai numerik berada dalam rentang bisnis yang valid? Misalnya umur
[0,150], port[1,65535]. - Karakter: Apakah ada karakter yang dilarang? Misalnya null byte, karakter kontrol, atau tanda baca yang membahayakan shell.