[Peringatan Keamanan] CVE-2026-31431 "Copy Fail" — Penjelasan Dampak bagi Pengguna QNAP NAS

QNAP saat ini sedang menyelidiki CVE-2026-31431, yang juga dikenal sebagai Copy Fail, dan kami ingin memberikan klarifikasi bagi pengguna yang mungkin khawatir terhadap dampaknya pada perangkat QNAP NAS.

Singkatnya, sebagian besar model QNAP NAS tidak terdampak kerentanan ini.

Masalah ini hanya memengaruhi model QNAP NAS berbasis ARM tertentu yang menjalankan versi kernel Linux spesifik. Berdasarkan penilaian kami saat ini:

  • Semua model QNAP NAS berbasis x86 tidak terdampak.
  • Model NAS berbasis ARM yang menjalankan QTS 4.x tidak terdampak.
  • Masalah ini hanya berlaku untuk model NAS berbasis ARM tertentu yang menjalankan versi kernel yang terdampak.

Silakan merujuk pada Security Advisory resmi QNAP untuk informasi terbaru:

https://www.qnap.com/go/security-advisory/qsa-26-16

Tentang kerentanan ini

Kerentanan ini merupakan masalah pendakian hak istimewa lokal (local privilege escalation).

Artinya, penyerang harus terlebih dahulu dapat menjalankan kode pada NAS sebagai pengguna biasa (bukan administrator) sebelum mencoba mengeksploitasi kerentanan. Ini bukan kerentanan yang dapat dieksploitasi langsung dari internet tanpa memperoleh akses lokal terlebih dahulu.

Untuk perangkat QNAP NAS, akses SSH dan Telnet secara default dibatasi untuk pengguna dalam grup administrator. Namun, pengguna tetap disarankan untuk meninjau eksposur sistem dan aplikasi, terutama jika menjalankan layanan atau kontainer yang dapat diakses oleh pengguna lain atau dari jaringan eksternal.

Tindakan yang direkomendasikan

Untuk mengurangi risiko secara umum, kami merekomendasikan hal-hal berikut:

  • Jangan memberikan akses shell kepada pengguna non-administrator kecuali benar-benar diperlukan.
  • Hanya jalankan image kontainer dari sumber tepercaya.
  • Tinjau pengaturan Container Station dan hindari memberikan akses yang tidak perlu kepada pengguna ke dalam kontainer.
  • Pastikan aplikasi, kontainer, dan layanan selalu diperbarui.
  • Nonaktifkan layanan dan aplikasi yang tidak digunakan.
  • Jika Web Server bawaan tidak digunakan, pertimbangkan untuk menonaktifkannya di Control Panel > Web Server.
  • Tempatkan NAS di belakang firewall dan hindari menghubungkannya langsung ke internet.
  • Ikuti security advisory resmi dan instal pembaruan keamanan segera setelah tersedia.

QNAP sedang mempersiapkan pembaruan keamanan dan akan memperbarui advisory ketika informasi atau perbaikan sudah tersedia.

Jika Anda memiliki konfigurasi sistem spesifik yang ingin ditanyakan, silakan hubungi Dukungan QNAP untuk bantuan lebih lanjut.

Demi keamanan Anda, mohon untuk tidak mempublikasikan informasi sensitif di forum, termasuk alamat IP publik, nama pengguna, nomor seri perangkat, log lengkap, atau konfigurasi sistem secara rinci.

— Tim Komunitas QNAP
Berdasarkan informasi dari QNAP Product Security Incident Response Team

Pertanyaan tentang container: Banyak container berjalan di bawah versi Linux seperti Alpine atau BusyBox, atau yang serupa. Karena container-container tersebut menjalankan “Linux yang berbeda” dari QNAP, apakah ada kerentanan di dalam container-container tersebut? Atau bukan begitu kasusnya?

Kontainer yang berjalan di QNAP NAS menggunakan kernel yang sama dengan proses lain, jadi jika NAS Anda terdampak, kami sarankan Anda memberi perhatian ekstra pada hal ini.

Oke. Jadi kalau NAS saya tidak terpengaruh, berarti container-container saya juga bakal aman, kan?

Berkat AI, saya bisa mendapatkan skrip pengujian dengan cepat.

Saya secara acak memilih sebuah h874 dengan QTS 5.2.8 (2025/12/25) dan menjalankan skrip ini yang dihasilkan dari Claude, lalu dicek ulang oleh Codex untuk memverifikasi apakah entrypoint-nya ada di QTS:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""CVE-2026-31431 surface check. Python 2.7 / 3.x compatible. Bind-only, safe."""
from __future__ import print_function
import os, socket, sys, errno

AF_ALG, SOCK_SEQPACKET = 38, 5

def in_container():
    if os.path.exists("/.dockerenv"):
        return True
    try:
        with open("/proc/1/cgroup") as f:
            d = f.read()
        return any(k in d for k in ("docker", "lxc", "kubepods", "containerd"))
    except (IOError, OSError):
        return False

def kernel_info():
    u = os.uname()
    # u: (sysname, nodename, release, version, machine)
    print("[*] Kernel:    {0} ({1})".format(u[2], u[4]))
    print("[*] Container: {0}".format(in_container()))

def kconfig_probe():
    paths = ["/proc/config.gz", "/boot/config-" + os.uname()[2]]
    keys = ("CONFIG_CRYPTO_USER_API_AEAD", "CONFIG_CRYPTO_USER_API",
            "CONFIG_CRYPTO_AUTHENC")
    for p in paths:
        if not os.path.exists(p):
            continue
        try:
            if p.endswith(".gz"):
                import gzip
                f = gzip.open(p, "rt")
            else:
                f = open(p, "r")
            data = f.read()
            f.close()
        except Exception as e:
            print("[!] kconfig read fail {0}: {1}".format(p, e))
            continue
        print("[*] kconfig source: {0}".format(p))
        for line in data.splitlines():
            for k in keys:
                if line.startswith(k + "=") or line.startswith("# " + k + " "):
                    print("    {0}".format(line))
        return
    print("[*] kconfig: not exposed (no /proc/config.gz, no /boot/config-*)")

def module_state():
    loaded = False
    try:
        with open("/proc/modules") as f:
            for line in f:
                if line.startswith("algif_aead "):
                    loaded = True
                    break
    except (IOError, OSError) as e:
        print("[!] /proc/modules: {0}".format(e))
    sysmod = os.path.isdir("/sys/module/algif_aead")
    print("[*] algif_aead loaded:  {0}".format(loaded))
    print("[*] /sys/module/algif_aead present: {0}".format(sysmod))
    bl = []
    for d in ("/etc/modprobe.d", "/run/modprobe.d", "/lib/modprobe.d"):
        if not os.path.isdir(d):
            continue
        try:
            entries = os.listdir(d)
        except OSError:
            continue
        for fn in entries:
            full = os.path.join(d, fn)
            try:
                with open(full) as f:
                    if "algif_aead" in f.read():
                        bl.append(full)
            except (IOError, OSError):
                pass
    print("[*] blacklist refs:     {0}".format(bl if bl else "none"))
    return loaded, sysmod

def afalg_bind_probe():
    try:
        s = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
    except socket.error as e:
        en = e.args[0] if e.args else 0
        print("[+] AF_ALG socket() blocked: errno={0} ({1}) {2}".format(
            en, errno.errorcode.get(en, "?"), os.strerror(en) if en else ""))
        return False
    try:
        s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
        print("[!] authencesn bind SUCCEEDED -- vulnerable surface EXPOSED")
        s.close()
        return True
    except socket.error as e:
        en = e.args[0] if e.args else 0
        print("[+] authencesn bind failed: errno={0} ({1}) {2}".format(
            en, errno.errorcode.get(en, "?"), os.strerror(en) if en else ""))
        s.close()
        return False

def userns_state():
    for p in ("/proc/sys/kernel/unprivileged_userns_clone",
              "/proc/sys/user/max_user_namespaces",
              "/proc/sys/kernel/apparmor_restrict_unprivileged_userns"):
        if os.path.exists(p):
            try:
                with open(p) as f:
                    print("[*] {0} = {1}".format(p, f.read().strip()))
            except (IOError, OSError):
                pass

def main():
    kernel_info()
    kconfig_probe()
    loaded, sysmod = module_state()
    exposed = afalg_bind_probe()
    userns_state()
    print("")
    if exposed:
        print("VERDICT: VULNERABLE attack surface present.")
        if in_container():
            print("         In container -> Part 2 (host page-cache write) reachable.")
        sys.exit(2)
    elif not (loaded or sysmod):
        print("VERDICT: Module absent and bind blocked.")
        sys.exit(0)
    else:
        print("VERDICT: Module present but bind blocked -- review.")
        sys.exit(1)

if __name__ == "__main__":
    main()

Dan hasilnya:

Dari host:

[qadmin@874QTS ~]$ uname -a
Linux 874QTS 5.10.60-qnap #1 SMP Thu Dec 25 01:08:48 CST 2025 x86_64 GNU/Linux
[qadmin@874QTS ~]$ python cpftest.py 
[*] Kernel:    5.10.60-qnap (x86_64)
[*] Container: False
[*] kconfig: not exposed (no /proc/config.gz, no /boot/config-*)
[*] algif_aead loaded:  False
[*] /sys/module/algif_aead present: False
[*] blacklist refs:     none
[+] AF_ALG socket() blocked: errno=97 (EAFNOSUPPORT) Address family not supported by protocol
[*] /proc/sys/user/max_user_namespaces = 127243

VERDICT: Module absent and bind blocked.

Dari sebuah container python di dalam NAS yang sama:

root@1ebc107603de:/home# uname -a
Linux 1ebc107603de 5.10.60-qnap #1 SMP Thu Dec 25 01:08:48 CST 2025 x86_64 GNU/Linux
root@1ebc107603de:/home# python cpftest.py 
[*] Kernel:    5.10.60-qnap (x86_64)
[*] Container: True
[*] kconfig: not exposed (no /proc/config.gz, no /boot/config-*)
[*] algif_aead loaded:  False
[*] /sys/module/algif_aead present: False
[*] blacklist refs:     none
[+] AF_ALG socket() blocked: errno=97 (EAFNOSUPPORT) Address family not supported by protocol
[*] /proc/sys/user/max_user_namespaces = 127243

VERDICT: Module absent and bind blocked.

Jadi kelihatannya aman bahkan menggunakan QTS versi lama.

Catatan: Saya bukan anggota profesional tim keamanan. Hanya berbagi pemikiran saja.

Lucu juga. Saya sering lihat orang mengeluh soal QNAP yang pakai versi lama dari perpustakaan ini atau itu, dan lain-lain. Tapi ternyata justru versi kernel Linux yang lebih baru yang punya kerentanan ini!