Aplikasi Command Line Interface (CLI) untuk visualisasi data tabular seperti grafik ASCII atau tabel interaktif di terminal sering kali mengasumsikan input file lokal berada dalam kendali pengguna tepercaya. Namun, ketika CLI memproses dataset tidak tepercaya (untrusted CSV) yang diunduh dari web atau dioperasikan dalam pipeline otomatis, kerentanan seperti alokasi memori tak terbatas (Denial of Service) dan Terminal Escape Injection (ANSI escape sequences) dapat membahayakan integritas terminal serta ketersediaan sistem.

Artikel ini membahas praktik hardening parser dan visualizer terminal di Rust menggunakan pendekatan streaming terikat, pembersihan karakter kontrol, serta konversi tipe data numerik yang aman.

Vektor Serangan Input CSV pada Aplikasi CLI

Saat membangun tool berbasis Rust seperti clone xan, serangan tipikal pada parsing dan rendering CSV mencakup tiga area utama:

  • Memory Exhaustion (OOM): File CSV dengan baris atau field tunggal berukuran gigabyte tanpa batas delimitasi dapat memaksa buffer memori membengkak hingga proses dimatikan oleh OOM Killer sistem operasi.
  • Terminal Escape Injection (ANSI Bombs): Kolom teks yang mengandung sekuens escape seperti \x1B]0;...\x07 atau manipulasi kursor ANSI dapat mengubah judul window terminal, menyembunyikan teks, atau memanipulasi buffer terminal saat dicetak ke stdout.
  • Numeric Parsing DoS: Parsing string arbitrer ke tipe floating point standar (f64) yang tidak menangani status nilai khusus seperti NaN, Infinity, atau representasi floating-point non-standar yang memicu kalkulasi tak terduga pada skala visualisasi.

1. Mitigasi Alokasi Memori dengan Bounded Streaming

Membaca seluruh konten CSV ke dalam memori via std::fs::read_to_string adalah anti-pattern untuk CLI produksi. Kita wajib menggunakan pembacaan berbasis aliran (streaming) dengan batas eksplisit ukuran record dan kapasitas field menggunakan crate csv standar.

use csv::ReaderBuilder;
use std::fs::File;
use std::io::{self, BufReader, Read};

const MAX_RECORD_SIZE_BYTES: usize = 64 * 1024; // Batas per record: 64 KB

pub fn get_hardened_csv_reader(path: &str) -> io::Result<csv::Reader<impl Read>> {
    let file = File::open(path)?;
    // Bungkus pembacaan dengan buffer terbatas
    let buffered = BufReader::with_capacity(128 * 1024, file);

    let reader = ReaderBuilder::new()
        .has_headers(true)
        .flexible(false) // Tolak baris yang jumlah field-nya tidak konsisten
        .buffer_capacity(MAX_RECORD_SIZE_BYTES)
        .from_reader(buffered);

    Ok(reader)
}

Pengaturan flexible(false) memastikan anomali struktural pada baris CSV langsung menghasilkan error di awal eksekusi, mencegah visualisasi data yang korup.

2. Pencegahan Terminal Escape Injection

Karakter ANSI escape sequence memungkinkan teks input memanipulasi emulator terminal pengguna. Untuk visualisasi terminal yang aman, seluruh field string teks yang akan dicetak sebagai label atau header wajib disanitasi dari karakter kontrol ASCII (0x00-0x1F dan 0x7F), terutama byte ESC (0x1B).

pub fn sanitize_terminal_output(raw: &str) -> String {
    raw.chars()
        .filter_map(|c| {
            // Izinkan spasi biasa, tolak semua karakter kontrol ASCII
            if c.is_ascii_control() {
                None
            } else {
                Some(c)
            }
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_strip_ansi_and_controls() {
        let malicious_payload = "\x1B[31;1mRedText\x1B[0m\nInject";
        let clean = sanitize_terminal_output(malicious_payload);
        assert_eq!(clean, "RedTextInject");
    }
}

Jika kebutuhan visualisasi memerlukan penanganan teks Unicode multibyte (misal: aksara non-Latin atau emoji), sanitasi harus tetap mempertahankan validitas UTF-8 sembari memfilter blok kontrol Unicode (C0 dan C1 control codes).

3. Safe Numeric Parsing untuk Plotting

Komponen plot grafik membutuhkan representasi numerik. Fungsi bawaan str::parse::<f64>() dapat meloloskan nilai NaN, inf, atau -inf yang merusak kalkulasi rentang minimum/maksimum kanvas terminal. Rust menyediakan method validasi numerik bawaan tanpa dependensi eksternal.

#[derive(Debug, PartialEq)]
pub enum NumericError {
    InvalidFormat,
    NonFiniteValue,
}

pub fn parse_safe_coordinate(input: &str) -> Result<f64, NumericError> {
    let val: f64 = input.trim().parse().map_err(|_| NumericError::InvalidFormat)?;

    // Cegah nilai tak terhingga dan Not-a-Number merusak auto-scaler visualisasi
    if !val.is_finite() {
        return Err(NumericError::NonFiniteValue);
    }

    Ok(val)
}

Implementasi Penuh Pipeline Pembacaan Terproteksi

Berikut adalah implementasi minimal integrasi pembacaan, validasi, dan sanitasi data yang siap dirender ke kanvas visualisasi terminal:

use csv::StringRecord;
use std::error::Error;

pub struct ValidatedRow {
    pub label: String,
    pub value: f64,
}

pub fn process_csv_for_chart(path: &str) -> Result<Vec<ValidatedRow>, Box<dyn Error>> {
    let mut reader = get_hardened_csv_reader(path)?;
    let mut record = StringRecord::new();
    let mut results = Vec::new();

    // Membaca record demi record menggunakan buffer yang dialokasikan sekali
    while reader.read_record(&mut record)? {
        if record.len() < 2 {
            continue; // Abaikan atau lempar format error
        }

        let raw_label = &record[0];
        let raw_value = &record[1];

        let label = sanitize_terminal_output(raw_label);
        let value = parse_safe_coordinate(raw_value)?;

        results.push(ValidatedRow { label, value });
    }

    Ok(results)
}

Catatan Kinerja: Hindari memanggil reader.records() iterator langsung dalam loop jika dataset berukuran masif, karena metode tersebut mengalokasikan StringRecord baru pada setiap baris. Pendekatan reader.read_record(&mut record) mendaur ulang buffer alokasi internal secara efisien.

Ringkasan Trade-Off

Penerapan validasi ketat ini memiliki konsekuensi fungsionalitas:

  • Penolakan Format Eksotis: Menghapus karakter kontrol akan menghilangkan format styling sah jika CSV memang sengaja memuat format escape terformat (kasus sangat langka).
  • Validitas Floating Point: Menolak NaN memutus kontinuitas plot garis data kosong. Jika nilai kosong diizinkan, transformasikan NaN menjadi Option<f64>::None alih-alih melempar error langsung.

Menerapkan pembatasan alokasi, pembersihan kontrol karakter terminal, dan pengetatan floating-point menghasilkan CLI visualisasi Rust yang tangguh, deterministik, dan aman terhadap eksploitasi data lokal maupun pipeline otomatis.