본문 바로가기
프로그래밍 언어 실전 가이드

루아 입문 시리즈 #19: 루아 보안 프로그래밍

by devcomet 2025. 7. 5.
728x90
반응형

Lua security programming guide thumbnail with shield and lock icons representing secure coding practices
루아 입문 시리즈 #19: 루아 보안 프로그래밍

루아 애플리케이션의 보안을 강화하기 위한 샌드박스 구성, 입력 검증, 그리고 보안 취약점 방지 기법을 완벽 가이드와 함께 실무에서 바로 적용할 수 있는 방법을 제공합니다.


루아 보안 프로그래밍의 중요성

루아(Lua)는 경량성과 유연성으로 인해 다양한 애플리케이션에서 스크립팅 언어로 활용되고 있습니다.

게임 개발, 웹 애플리케이션, 네트워크 애플리케이션 등 다양한 분야에서 루아를 사용하는 만큼, 보안에 대한 고려사항도 중요해지고 있습니다.

특히 사용자 입력을 처리하거나 외부 스크립트를 실행할 때는 보안 취약점이 발생할 수 있으며, 이를 방지하기 위한 체계적인 접근이 필요합니다.

루아 보안 프로그래밍의 핵심은 샌드박스 환경 구성, 입력 검증, 권한 제한 등의 다층적 보안 전략을 구현하는 것입니다.


루아 샌드박스 환경 구성

기본 샌드박스 개념

샌드박스는 루아 스크립트가 실행될 수 있는 격리된 환경을 의미합니다.

이를 통해 악의적인 스크립트나 오류가 있는 코드가 시스템 전체에 영향을 미치는 것을 방지할 수 있습니다.

-- 기본 샌드박스 환경 구성
local sandbox = {}
sandbox._G = sandbox

-- 허용된 함수들만 포함
sandbox.print = print
sandbox.tostring = tostring
sandbox.tonumber = tonumber
sandbox.type = type
sandbox.pairs = pairs
sandbox.ipairs = ipairs
sandbox.next = next
sandbox.pcall = pcall
sandbox.xpcall = xpcall

-- 수학 함수들
sandbox.math = {
    abs = math.abs,
    floor = math.floor,
    ceil = math.ceil,
    max = math.max,
    min = math.min,
    pi = math.pi
}

-- 문자열 함수들
sandbox.string = {
    sub = string.sub,
    len = string.len,
    upper = string.upper,
    lower = string.lower,
    gsub = string.gsub,
    match = string.match,
    find = string.find
}

위험한 함수 차단

시스템에 직접적인 영향을 줄 수 있는 함수들은 샌드박스에서 제외해야 합니다.

-- 차단해야 할 위험한 함수들
local dangerous_functions = {
    "os",           -- 운영체제 접근
    "io",           -- 파일 입출력
    "debug",        -- 디버그 기능
    "package",      -- 패키지 로딩
    "require",      -- 모듈 로딩
    "loadfile",     -- 파일에서 코드 로딩
    "dofile",       -- 파일 실행
    "load",         -- 문자열에서 코드 로딩
    "loadstring",   -- 동적 코드 실행
    "rawget",       -- 메타테이블 우회
    "rawset",       -- 메타테이블 우회
    "rawequal",     -- 메타테이블 우회
    "rawlen",       -- 메타테이블 우회
    "getmetatable", -- 메타테이블 접근
    "setmetatable"  -- 메타테이블 설정
}

-- 샌드박스에서 위험한 함수 제거
for _, func_name in ipairs(dangerous_functions) do
    sandbox[func_name] = nil
end

 

Lua sandbox security architecture diagram showing isolated execution environment with allowed and blocked functions
루아 샌드박스 환경의 구조를 보여주는 다이어그램

고급 샌드박스 구현

더 정교한 샌드박스를 구현하려면 메타테이블을 활용하여 접근 제어를 강화할 수 있습니다.

-- 고급 샌드박스 구현
function create_secure_sandbox()
    local sandbox = {}
    local allowed_globals = {
        "print", "tostring", "tonumber", "type", "pairs", "ipairs",
        "next", "pcall", "xpcall", "error", "assert", "select",
        "unpack", "table", "string", "math"
    }

    -- 허용된 전역 변수만 접근 가능
    local mt = {
        __index = function(t, k)
            if table.find(allowed_globals, k) then
                return _G[k]
            else
                error("Access to '" .. k .. "' is not permitted", 2)
            end
        end,
        __newindex = function(t, k, v)
            -- 새로운 전역 변수 생성 방지
            if not table.find(allowed_globals, k) then
                error("Creating global variable '" .. k .. "' is not permitted", 2)
            end
            rawset(t, k, v)
        end
    }

    setmetatable(sandbox, mt)
    return sandbox
end

-- 샌드박스에서 코드 실행
function execute_in_sandbox(code, sandbox_env)
    local func, err = load(code, "sandbox", "t", sandbox_env)
    if not func then
        return nil, "Syntax error: " .. err
    end

    local success, result = pcall(func)
    if not success then
        return nil, "Runtime error: " .. result
    end

    return result
end

입력 검증 및 필터링

기본 입력 검증

사용자 입력을 처리할 때는 항상 검증 과정을 거쳐야 합니다.

-- 입력 검증 함수들
local InputValidator = {}

function InputValidator.validate_string(input, max_length, pattern)
    -- nil 체크
    if not input then
        return false, "Input cannot be nil"
    end

    -- 타입 체크
    if type(input) ~= "string" then
        return false, "Input must be a string"
    end

    -- 길이 체크
    if max_length and #input > max_length then
        return false, "Input too long (max: " .. max_length .. ")"
    end

    -- 패턴 검증
    if pattern and not string.match(input, pattern) then
        return false, "Input format is invalid"
    end

    return true, input
end

function InputValidator.validate_number(input, min_val, max_val)
    local num = tonumber(input)
    if not num then
        return false, "Input is not a valid number"
    end

    if min_val and num < min_val then
        return false, "Number too small (min: " .. min_val .. ")"
    end

    if max_val and num > max_val then
        return false, "Number too large (max: " .. max_val .. ")"
    end

    return true, num
end

-- 사용 예시
local user_input = "12345"
local is_valid, result = InputValidator.validate_string(user_input, 10, "^%d+$")
if is_valid then
    print("Valid input:", result)
else
    print("Invalid input:", result)
end

SQL 인젝션 방지

데이터베이스 쿼리를 구성할 때는 SQL 인젝션 공격을 방지해야 합니다.

-- SQL 인젝션 방지를 위한 이스케이프 함수
function escape_sql_string(str)
    if not str then return "NULL" end

    -- 작은따옴표 이스케이프
    str = string.gsub(str, "'", "''")

    -- 백슬래시 이스케이프
    str = string.gsub(str, "\\", "\\\\")

    -- 널 바이트 제거
    str = string.gsub(str, "\0", "")

    return "'" .. str .. "'"
end

-- 파라미터화된 쿼리 생성
function build_safe_query(template, params)
    local safe_params = {}
    for i, param in ipairs(params) do
        if type(param) == "string" then
            safe_params[i] = escape_sql_string(param)
        elseif type(param) == "number" then
            safe_params[i] = tostring(param)
        else
            safe_params[i] = "NULL"
        end
    end

    return string.format(template, unpack(safe_params))
end

-- 사용 예시
local query_template = "SELECT * FROM users WHERE username = %s AND age > %s"
local safe_query = build_safe_query(query_template, {"john'; DROP TABLE users; --", 18})
print(safe_query)
-- 출력: SELECT * FROM users WHERE username = 'john''; DROP TABLE users; --' AND age > 18

 

Lua input validation process flowchart demonstrating security filtering steps from user input to sanitized output
입력 검증 프로세스 플로우차트

정규표현식을 통한 입력 필터링

정규표현식을 사용하여 다양한 형태의 입력을 검증할 수 있습니다.

-- 정규표현식 패턴 모음
local ValidationPatterns = {
    email = "^[%w%._%+%-]+@[%w%._%+%-]+%.%w+$",
    phone = "^%d{3}%-%d{4}%-%d{4}$",
    username = "^[%w_]{3,20}$",
    password = "^.{8,}$",  -- 최소 8자
    ip_address = "^%d+%.%d+%.%d+%.%d+$",
    url = "^https?://[%w%._%+%-/]+$"
}

-- 입력 검증기
function validate_input(input, validation_type)
    local pattern = ValidationPatterns[validation_type]
    if not pattern then
        return false, "Unknown validation type"
    end

    if not input or type(input) ~= "string" then
        return false, "Invalid input type"
    end

    if string.match(input, pattern) then
        return true, input
    else
        return false, "Input does not match required format"
    end
end

-- 사용 예시
local email = "user@example.com"
local is_valid, result = validate_input(email, "email")
print("Email validation:", is_valid, result)

보안 취약점 방지 기법

코드 인젝션 방지

동적 코드 실행 시 코드 인젝션을 방지하는 것이 중요합니다.

-- 안전한 코드 실행기
local SafeExecutor = {}

function SafeExecutor.execute_safe_code(code, whitelist)
    -- 위험한 키워드 검사
    local dangerous_keywords = {
        "os%.", "io%.", "debug%.", "package%.", "require", 
        "loadfile", "dofile", "load", "loadstring"
    }

    for _, keyword in ipairs(dangerous_keywords) do
        if string.find(code, keyword) then
            return false, "Dangerous keyword detected: " .. keyword
        end
    end

    -- 화이트리스트 검사
    if whitelist then
        local words = {}
        for word in string.gmatch(code, "[%w_]+") do
            words[word] = true
        end

        for word in pairs(words) do
            if not whitelist[word] then
                return false, "Unauthorized identifier: " .. word
            end
        end
    end

    -- 샌드박스에서 실행
    local sandbox = create_secure_sandbox()
    local func, err = load(code, "safe_code", "t", sandbox)
    if not func then
        return false, "Syntax error: " .. err
    end

    local success, result = pcall(func)
    if not success then
        return false, "Runtime error: " .. result
    end

    return true, result
end

-- 사용 예시
local user_code = [[
    local result = 0
    for i = 1, 10 do
        result = result + i
    end
    return result
]]

local whitelist = {
    ["local"] = true, ["result"] = true, ["for"] = true,
    ["i"] = true, ["do"] = true, ["end"] = true, ["return"] = true
}

local success, result = SafeExecutor.execute_safe_code(user_code, whitelist)
print("Code execution result:", success, result)

메모리 및 CPU 사용량 제한

무한 루프나 과도한 메모리 사용을 방지하기 위한 리소스 제한을 구현할 수 있습니다.

-- 실행 시간 제한
function execute_with_timeout(code, timeout_seconds)
    local start_time = os.time()
    local sandbox = create_secure_sandbox()

    -- 타임아웃 체크 훅 설정
    local function timeout_hook()
        if os.time() - start_time > timeout_seconds then
            error("Execution timeout")
        end
    end

    local func, err = load(code, "timed_code", "t", sandbox)
    if not func then
        return false, "Syntax error: " .. err
    end

    -- 디버그 훅 설정 (일정 간격으로 타임아웃 체크)
    debug.sethook(timeout_hook, "", 1000)

    local success, result = pcall(func)
    debug.sethook()  -- 훅 제거

    if not success then
        return false, "Runtime error: " .. result
    end

    return true, result
end

-- 메모리 사용량 모니터링
function monitor_memory_usage(code, max_memory_mb)
    local initial_memory = collectgarbage("count")

    local sandbox = create_secure_sandbox()
    local func, err = load(code, "memory_monitored", "t", sandbox)
    if not func then
        return false, "Syntax error: " .. err
    end

    local success, result = pcall(func)

    local final_memory = collectgarbage("count")
    local memory_used = final_memory - initial_memory

    if memory_used > max_memory_mb * 1024 then
        return false, "Memory limit exceeded: " .. memory_used .. "KB"
    end

    if not success then
        return false, "Runtime error: " .. result
    end

    return true, result, memory_used
end

 

Multi-layered Lua security defense mechanism diagram showing input validation, sandbox isolation, and resource protection layers
보안 계층 구조 및 방어 메커니즘 다이어그램


실무 보안 체크리스트

개발 단계 보안 점검

항목 설명 우선순위
입력 검증 모든 사용자 입력에 대한 검증 로직 구현 높음
샌드박스 구성 안전한 실행 환경 구성 높음
권한 제한 최소 권한 원칙 적용 높음
에러 처리 민감한 정보 노출 방지 중간
로깅 보안 이벤트 로그 기록 중간
암호화 민감한 데이터 암호화 높음

배포 전 최종 점검

-- 보안 체크리스트 자동화 도구
local SecurityChecker = {}

function SecurityChecker.check_code_security(code)
    local issues = {}

    -- 1. 위험한 함수 사용 검사
    local dangerous_patterns = {
        "os%.[%w_]+", "io%.[%w_]+", "debug%.[%w_]+",
        "require", "loadfile", "dofile", "load", "loadstring"
    }

    for _, pattern in ipairs(dangerous_patterns) do
        if string.find(code, pattern) then
            table.insert(issues, "Dangerous function detected: " .. pattern)
        end
    end

    -- 2. 하드코딩된 민감한 정보 검사
    local sensitive_patterns = {
        "password%s*=%s*[\"'][^\"']+[\"']",
        "api_key%s*=%s*[\"'][^\"']+[\"']",
        "secret%s*=%s*[\"'][^\"']+[\"']"
    }

    for _, pattern in ipairs(sensitive_patterns) do
        if string.find(code, pattern) then
            table.insert(issues, "Hardcoded sensitive information detected")
        end
    end

    -- 3. SQL 인젝션 취약점 검사
    if string.find(code, "SELECT.+%.%.") or string.find(code, "INSERT.+%.%.") then
        table.insert(issues, "Potential SQL injection vulnerability")
    end

    return issues
end

-- 사용 예시
local code_to_check = [[
    local password = "admin123"
    local query = "SELECT * FROM users WHERE id = " .. user_id
    os.execute("rm -rf /")
]]

local security_issues = SecurityChecker.check_code_security(code_to_check)
for _, issue in ipairs(security_issues) do
    print("Security Issue:", issue)
end

로깅 및 모니터링

보안 이벤트 로깅

보안 관련 이벤트를 체계적으로 로깅하여 보안 위협을 조기에 감지할 수 있습니다.

-- 보안 로거 구현
local SecurityLogger = {}
SecurityLogger.log_file = "security.log"

function SecurityLogger.log_security_event(event_type, message, severity)
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local log_entry = string.format("[%s] [%s] [%s] %s\n", 
        timestamp, severity, event_type, message)

    local file = io.open(SecurityLogger.log_file, "a")
    if file then
        file:write(log_entry)
        file:close()
    end

    -- 심각한 보안 이벤트의 경우 즉시 알림
    if severity == "CRITICAL" then
        SecurityLogger.send_alert(event_type, message)
    end
end

function SecurityLogger.send_alert(event_type, message)
    -- 실제 환경에서는 이메일, SMS, 또는 모니터링 시스템으로 알림
    print("SECURITY ALERT:", event_type, message)
end

-- 사용 예시
SecurityLogger.log_security_event("INJECTION_ATTEMPT", 
    "SQL injection detected in user input", "HIGH")
SecurityLogger.log_security_event("UNAUTHORIZED_ACCESS", 
    "Attempt to access restricted function", "MEDIUM")

침입 탐지 시스템

간단한 침입 탐지 시스템을 구현하여 의심스러운 활동을 모니터링할 수 있습니다.

-- 침입 탐지 시스템
local IntrusionDetector = {}
IntrusionDetector.failed_attempts = {}
IntrusionDetector.max_attempts = 5
IntrusionDetector.lockout_duration = 300  -- 5분

function IntrusionDetector.record_failed_attempt(ip_address, attempt_type)
    local current_time = os.time()

    if not IntrusionDetector.failed_attempts[ip_address] then
        IntrusionDetector.failed_attempts[ip_address] = {}
    end

    table.insert(IntrusionDetector.failed_attempts[ip_address], {
        time = current_time,
        type = attempt_type
    })

    -- 오래된 시도 기록 정리
    IntrusionDetector.cleanup_old_attempts(ip_address, current_time)

    -- 임계값 초과 검사
    local recent_attempts = #IntrusionDetector.failed_attempts[ip_address]
    if recent_attempts >= IntrusionDetector.max_attempts then
        SecurityLogger.log_security_event("INTRUSION_DETECTED",
            "Multiple failed attempts from IP: " .. ip_address, "CRITICAL")
        return true  -- 차단 필요
    end

    return false
end

function IntrusionDetector.cleanup_old_attempts(ip_address, current_time)
    local attempts = IntrusionDetector.failed_attempts[ip_address]
    local valid_attempts = {}

    for _, attempt in ipairs(attempts) do
        if current_time - attempt.time < IntrusionDetector.lockout_duration then
            table.insert(valid_attempts, attempt)
        end
    end

    IntrusionDetector.failed_attempts[ip_address] = valid_attempts
end

function IntrusionDetector.is_blocked(ip_address)
    local attempts = IntrusionDetector.failed_attempts[ip_address]
    if not attempts then return false end

    local current_time = os.time()
    IntrusionDetector.cleanup_old_attempts(ip_address, current_time)

    return #attempts >= IntrusionDetector.max_attempts
end

암호화 및 해싱

패스워드 해싱

사용자 패스워드를 안전하게 저장하기 위한 해싱 기법을 구현합니다.

-- 간단한 해싱 함수 (실제 환경에서는 더 강력한 해싱 라이브러리 사용 권장)
local CryptoUtil = {}

function CryptoUtil.simple_hash(data, salt)
    salt = salt or ""
    local hash = 0
    local combined = data .. salt

    for i = 1, #combined do
        hash = (hash * 31 + string.byte(combined, i)) % 2147483647
    end

    return string.format("%x", hash)
end

function CryptoUtil.generate_salt()
    local salt = ""
    for i = 1, 16 do
        salt = salt .. string.char(math.random(97, 122))
    end
    return salt
end

function CryptoUtil.hash_password(password)
    local salt = CryptoUtil.generate_salt()
    local hash = CryptoUtil.simple_hash(password, salt)
    return hash .. ":" .. salt
end

function CryptoUtil.verify_password(password, stored_hash)
    local hash, salt = string.match(stored_hash, "([^:]+):([^:]+)")
    if not hash or not salt then
        return false
    end

    local computed_hash = CryptoUtil.simple_hash(password, salt)
    return computed_hash == hash
end

-- 사용 예시
local password = "mySecurePassword123"
local hashed = CryptoUtil.hash_password(password)
print("Hashed password:", hashed)

local is_valid = CryptoUtil.verify_password(password, hashed)
print("Password valid:", is_valid)

데이터 암호화

민감한 데이터를 암호화하여 저장하는 방법을 구현합니다.

-- 간단한 XOR 암호화 (실제 환경에서는 AES 등 강력한 암호화 사용 권장)
local SimpleEncryption = {}

function SimpleEncryption.encrypt(data, key)
    local encrypted = {}
    local key_length = #key

    for i = 1, #data do
        local key_char = string.byte(key, ((i - 1) % key_length) + 1)
        local data_char = string.byte(data, i)
        encrypted[i] = string.char(data_char ~ key_char)
    end

    return table.concat(encrypted)
end

function SimpleEncryption.decrypt(encrypted_data, key)
    -- XOR 암호화는 복호화도 같은 연산
    return SimpleEncryption.encrypt(encrypted_data, key)
end

-- Base64 인코딩 (데이터 전송용)
function SimpleEncryption.base64_encode(data)
    local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    local result = ""

    for i = 1, #data, 3 do
        local a = string.byte(data, i) or 0
        local b = string.byte(data, i + 1) or 0
        local c = string.byte(data, i + 2) or 0

        local bitmap = (a << 16) | (b << 8) | c

        result = result .. string.char(
            string.byte(chars, ((bitmap >> 18) & 63) + 1),
            string.byte(chars, ((bitmap >> 12) & 63) + 1),
            string.byte(chars, ((bitmap >> 6) & 63) + 1),
            string.byte(chars, (bitmap & 63) + 1)
        )
    end

    -- 패딩 처리
    local padding = (3 - (#data % 3)) % 3
    if padding > 0 then
        result = string.sub(result, 1, -padding - 1) .. string.rep("=", padding)
    end

    return result
end

-- 사용 예시
local sensitive_data = "This is sensitive information"
local encryption_key = "mySecretKey123"

local encrypted = SimpleEncryption.encrypt(sensitive_data, encryption_key)
local encoded = SimpleEncryption.base64_encode(encrypted)
print("Encrypted and encoded:", encoded)

local decoded = SimpleEncryption.base64_decode(encoded)
local decrypted = SimpleEncryption.decrypt(decoded, encryption_key)
print("Decrypted:", decrypted)

마무리 및 권장사항

루아 보안 프로그래밍은 단일 기법만으로는 완전한 보안을 달성할 수 없습니다.

다층 보안 전략을 통해 샌드박스 환경 구성, 철저한 입력 검증, 적절한 권한 관리, 그리고 지속적인 모니터링을 결합해야 합니다.

특히 웹 애플리케이션이나 API 서버에서 루아를 사용할 때는 더욱 주의가 필요하며, 정기적인 보안 감사와 취약점 점검을 통해 보안 수준을 유지해야 합니다.

이번 글에서 다룬 보안 기법들을 실무에 적용할 때는 각 환경의 특성에 맞게 조정하여 사용하시기 바랍니다.

다음 시리즈에서는 루아의 성능 최적화 기법에 대해 다루어보겠습니다.


참고 자료:

이전 글: 루아 입문 시리즈 #18: 루아와 데이터베이스

 

루아 입문 시리즈 #18: 루아와 데이터베이스

루아 프로그래밍 언어를 활용한 MySQL, PostgreSQL, MongoDB 데이터베이스 연동 완전 가이드와 실전 예제로 데이터베이스 프로그래밍을 마스터하세요.현대 애플리케이션 개발에서 데이터베이스는 필

notavoid.tistory.com

 

728x90
반응형