웹 취약점 2026.11.10 · 조회 12

phpMyAdmin 노출 위험과 외부 접속 차단, Adminer 보안: 로그인 화면만 열려도 문제인 이유 [2026]

DB 포트를 막아 둔 서버도 phpMyAdmin이나 Adminer가 80/443 위에 올라 있으면 DB로 가는 문이 하나 남는다. 로그인 화면은 DB 계정 대입의 표적이고, Adminer의 CVE-2021-21311은 권한 불필요(PR:N)로 평가된 SSRF인데 2025년 9월 CISA의 악용 확인 목록에 올랐다. 노출 확인 명령과 삭제, IP 제한, 별도 인증, 최신 버전 순의 차단법을 정리했다.

S
보안 전문 기업 SENTRIX 레드팀
해킹대회 수상 · 보안 실무 10년 · 실전 진단 경험을 바탕으로 작성합니다.
웹사이트를 운영하고 있다면
보안 헤더·인증서·공개 노출부터 무료로 점검하세요. 결과 링크는 이메일로 받습니다. 로그인 이후 권한·업무 로직은 별도 검증이 필요합니다.

다음 화면에서 이메일과 점검 권한을 확인합니다. 회원가입·카드 없이 무료 요약을 받고, 상세 리포트는 선택 구매합니다.

DB 관리 도구 외부 노출은 phpMyAdmin이나 Adminer 같은 웹 기반 DB 관리 화면이 인터넷에서 누구에게나 열려 있는 상태로, 로그인 화면에 닿는 것만으로 DB 계정 대입과 도구 자체의 결함이 모두 공격 대상이 된다.
3306을 방화벽으로 막아 두고 안심한 서버라도, 80/443 위에 phpMyAdmin이나 Adminer 로그인 화면이 열려 있으면 DB로 가는 문이 하나 더 있는 셈이다. 관리 도구는 서버 안에서 DB에 붙기 때문에 포트 차단과 상관없이 동작하고, 로그인 화면은 DB 계정 비밀번호를 웹으로 대입해 볼 수 있는 창구가 된다. 도구 자체의 결함도 있다. Adminer의 CVE-2021-21311(SSRF)은 NVD가 권한 불필요(PR:N)로 평가한 결함인데, 2021년에 공개된 이 건을 미국 CISA가 실제 악용이 확인된 취약점 목록(KEV)에 2025년 9월 29일 올렸다. 개인정보를 담은 DB라면 접근 통제는 개인정보 보호법 제29조와 시행령 제30조제1항이 정한 안전성 확보 조치에도 들어간다.
이 글은 자주 발생하는 실수 패턴, 점검·조치 절차, 자가진단을 정리했다.

한눈에 보는 핵심

비밀번호가 강하면 로그인 화면이 열려 있어도 괜찮지 않나요?

비밀번호는 대입을 어렵게 할 뿐이고, 결함은 비밀번호와 무관하게 있을 수 있습니다. Adminer의 CVE-2021-21311은 영향 버전이 4.0.0부터 4.7.8까지이고 4.7.9에서 수정됐으며, 모든 DB 드라이버가 묶인 adminer.php가 대상이었습니다. NVD가 권한 불필요(PR:N)로 평가한 결함이라 로그인 성공이 전제가 아닙니다. 그래서 비밀번호를 올리는 일보다 도구를 쓰는 사람 말고는 화면에 닿지 못하게 막는 일을 먼저 해야 합니다.

경로 이름을 바꾸거나 403으로 막아 두면 충분한가요?

충분하지 않습니다. OWASP 웹 보안 테스트 가이드는 관리 화면을 찾는 방법으로 경로 추측, 클라이언트에 내려간 소스 속 링크, 기본 설정 문서, 다른 포트 확인을 들기 때문에 이름을 바꾸는 것은 시간을 벌어 주는 정도에 그칩니다. 403으로 막는 것은 한 단계 낫지만 파일과 설정이 그대로 남아 있어 규칙 한 줄이 바뀌면 다시 열립니다. 쓰지 않는 도구는 삭제하고, 쓰는 도구는 IP 제한과 인증을 겹쳐야 합니다.

접속 IP만 제한하면 끝인가요?

IP 제한은 효과가 큰 둘째 단계지만 혼자 두면 구멍이 남습니다. Adminer 공식 사이트는 비공개로 두는 방법으로 접속 허용 IP 목록, 웹서버 비밀번호, OTP 같은 보안 플러그인, 로그인 방식 변경을 나열합니다. 허용한 IP가 바뀌거나 여럿이 나눠 쓰는 공인 IP이면 범위가 의도보다 넓어지고, Cloudflare나 로드밸런서 뒤에서는 서버가 보는 주소가 프록시 주소라 제한이 무력해질 수 있습니다. 그래서 이 글은 IP 제한과 웹서버 인증을 겹치고, 프록시 뒤라면 접속자 IP를 복원한 뒤에 허용 목록을 쓰는 쪽을 권합니다.

자주 발생하는 실수 패턴

#패턴위험도
1개발 중 편하려고 올린 adminer.php나 서버 구축 때 같이 깔린 phpMyAdmin을 배포 뒤에도 그대로 둠치명
2AI 코딩 도구가 만든 docker-compose나 예제에 들어 있던 phpmyadmin·adminer 서비스를 8080:80처럼 모든 주소에 게시하고, ufw를 켰으니 막혔다고 믿음치명
3로그인 보호를 약하게 만드는 설정: AllowNoPassword 활성화, 비밀번호 없는 root·익명 DB 계정, config 인증(계정과 비밀번호를 config.inc.php에 기재)치명
4압축 파일로 직접 푼 phpMyAdmin과 단일 파일 adminer.php를 수년째 갱신하지 않고, setup·test 폴더도 지우지 않음높음
5경로 이름만 바꾸거나 403으로 막고 파일은 남겨 둠, 설정 한 줄이 바뀌면 다시 열림중간
6Cloudflare·로드밸런서 뒤에서 IP 제한을 걸었는데 서버가 보는 주소는 프록시 주소라 제한이 무력하거나 전원이 차단됨중간

접속 IP 제한과 웹서버 인증 설정 (nginx, Apache)

# 순서: 1) 쓰지 않으면 삭제  2) 접속 IP 제한  3) 웹서버 인증 한 겹  4) 최신 버전
# 아래는 2)·3) 단계다. IP, 경로, PHP 소켓은 모두 예시 값이니 서버에 맞게 바꾼다.
# 비밀번호 파일 생성: sudo htpasswd -c /etc/nginx/.htpasswd_dbadmin 운영자계정  (apache2-utils)

server {
    # (listen / server_name / root 등 기존 설정은 그대로 둔다)

    # phpMyAdmin: 허용 IP에서 왔고 비밀번호도 맞아야 통과한다
    # ^~ 가 없으면 서버 전체의 'location ~ \.php$' 가 요청을 먼저 가져가 이 규칙이 건너뛰어진다
    location ^~ /phpmyadmin/ {
        satisfy all;                    # IP와 비밀번호를 둘 다 통과해야 한다
        allow 203.0.113.10;             # 사무실 고정 IP (예시)
        allow 198.51.100.0/24;          # VPN 출구 대역 (예시)
        deny  all;                      # 나머지는 403
        auth_basic           "db admin";
        auth_basic_user_file /etc/nginx/.htpasswd_dbadmin;

        location ~ \.php$ {             # 안쪽 PHP 처리도 위 규칙을 상속받는다
            include snippets/fastcgi-php.conf;        # Debian/Ubuntu 기본 조각, 기존 PHP 설정과 같게
            fastcgi_pass unix:/run/php/php-fpm.sock;  # 서버마다 다른 값
        }
    }

    # 단일 파일 Adminer: = 는 일치하면 검색이 끝나므로 PHP 처리를 여기에 직접 적는다
    location = /adminer.php {
        satisfy all;
        allow 203.0.113.10;
        deny  all;
        auth_basic           "db admin";
        auth_basic_user_file /etc/nginx/.htpasswd_dbadmin;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

# Cloudflare·로드밸런서 뒤라면 $remote_addr 는 프록시 주소다. set_real_ip_from 과
# real_ip_header 로 접속자 IP를 복원한 뒤에 allow 를 쓴다 (realip 모듈이 빌드에 포함돼야 한다).
# Apache 2.4 는 <Location "/phpmyadmin"> 안에 아래를 둔다 (AuthType Basic 등 인증 지시자는 별도).
#   <RequireAll>
#     Require ip 203.0.113.10 198.51.100.0/24
#     Require valid-user
#   </RequireAll>

이 설정을 넣으면 허용 IP가 아닌 곳에서는 응답이 403이 된다. 403은 접근이 막혔다는 뜻일 뿐 도구가 남아 있다는 증거는 아니어서, 설치 여부는 서버에서 확인한다.
IP 제한과 인증은 접근하는 사람을 줄일 뿐 도구의 결함을 고치지 않으므로, 순서는 삭제, 제한, 인증, 갱신이다.

자가진단

# 내 IP를 허용 목록에 넣어 뒀다면 휴대폰 데이터처럼 다른 망에서 돌려야 바깥에서 보이는 모습이 나온다.
H=https://example.com

# 1) 이름 예시별 응답 코드 (리눅스 경로는 대소문자를 구분하고, 실제 이름은 설치한 사람이 정한다)
for p in phpmyadmin/ phpMyAdmin/ pma/ myadmin/ dbadmin/ mysql/ sql/ db/ \
         adminer.php adminer/ adminer/adminer.php tools/adminer.php; do
  c=$(curl -s -o /dev/null --max-time 10 -w '%{http_code}' "$H/$p")
  echo "$c  /$p"
done
# 판정: 200=열려 있을 수 있음(2단계로 확인), 401=인증을 요구함(2단계 셋째 명령으로 누구의 인증인지 확인),
#       403=접근이 막혀 있음(도구가 있는지는 이것만으로 알 수 없으니 3단계로 서버에서 확인), 404=이 경로에는 없음(다른 이름일 수 있음),
#       301·302=옮겨 간 주소를 따라가 다시 판정(curl -L)

# 2) 200이 진짜 도구 화면인지, 401이 누구의 인증인지 (adminer.php는 경로만 바꿔서 같은 방식으로)
curl -s --max-time 10 "$H/phpmyadmin/" | grep -io -m1 -E 'phpmyadmin|adminer'
curl -s -o /dev/null --max-time 10 -w '%{http_code}\n' "$H/no-such-path-9f3a"
curl -sI --max-time 10 "$H/phpmyadmin/" | grep -i 'www-authenticate'
# 판정: 첫 줄에 phpmyadmin 또는 adminer가 찍히면 실제 도구 화면이다.
#       둘째 줄이 200이면 모든 경로에 200을 주는 사이트라 1단계의 200은 의미가 없다.
#       셋째 줄의 realm이 내가 웹서버에 적은 문구가 아니면 도구 자체의 HTTP 인증(DB 계정으로 로그인)일 수 있다.

# 3) 서버 안의 설치 흔적 (SSH 접속 후)
sudo find / \( -path /proc -o -path /sys \) -prune -o \( -iname 'adminer*.php' -o -iname 'phpmyadmin*' \) -print 2>/dev/null
dpkg -l 2>/dev/null | grep -iE 'phpmyadmin|adminer'      # RHEL 계열은 rpm -qa | grep -iE 'phpmyadmin|adminer'
sudo grep -rIliE 'phpmyadmin|adminer' /etc/nginx /etc/apache2 /etc/httpd 2>/dev/null
# 판정: 하나라도 나오는데 지금 쓰는 사람이 없으면 삭제 대상이다. /var/lib/docker 아래 경로는 4단계로 간다.

# 4) Docker로 띄웠다면 어느 주소에 게시됐는가
docker ps --format '{{.Names}}  {{.Image}}  {{.Ports}}' | grep -iE 'phpmyadmin|adminer'
# 판정: 0.0.0.0:8080->80/tcp 처럼 0.0.0.0이나 :::가 보이면 모든 주소에 열려 있다.
#       ufw를 켜 놓았어도 Docker가 게시한 포트는 ufw 규칙보다 앞에서 처리된다.
#       127.0.0.1:8080->80/tcp 면 기본 구성에서는 서버 안에서만 접속된다 (compose에는 "127.0.0.1:8080:80" 으로 적는다).
#       다만 Docker 공식 문서는 28.0.0 미만 버전에서 같은 네트워크 구간의 다른 호스트가 이 포트에 닿을 수 있다고 안내하니
#       docker version 으로 버전을 확인하고, 컨테이너 주소로 직접 라우팅하는 구성이면 그쪽 접근도 따로 막는다.

# 5) phpMyAdmin 설정 (config.inc.php 위치부터 찾는다)
sudo find / -name 'config.inc.php' 2>/dev/null
CFG=/찾은/경로/config.inc.php      # 위에서 나온 경로로 바꾼다
grep -nE "auth_type|AllowNoPassword|AllowRoot|AllowDeny" "$CFG"
ls -d "$(dirname "$CFG")"/setup "$(dirname "$CFG")"/test 2>/dev/null
# 판정: auth_type이 'config'면 계정과 비밀번호가 파일에 적혀 있다(cookie 방식으로 바꾸고 파일 권한을 확인).
#       AllowNoPassword가 true면 즉시 false로 바꾼다. root 접속을 막으려면 AllowRoot를 false로 둔다.
#       setup, test 폴더가 보이면 삭제한다.

# 6) adminer.php는 버전부터 본다
ADM=/찾은/경로/adminer.php         # 3단계에서 나온 경로로 바꾼다
grep -m1 -i '@version' "$ADM"
# 판정: 4.7.9 미만이면 CVE-2021-21311 범위(4.0.0~4.7.8)다. 4.7.9 이상이어도 www.adminer.org 첫 화면의
#       최신 버전과 차이가 크면 교체하고, 버전이 안 나오면 지운다.

위 시그널이 발견되면 결함이다. phpMyAdmin 공식 설치 보안 문서를 함께 참조하라.

조치 절차

순위조치
1쓰지 않는 도구는 삭제한다. 패키지로 깔았다면 sudo apt purge phpmyadmin(RHEL 계열은 패키지 관리자의 제거 명령), 압축을 풀어 둔 폴더와 adminer.php는 파일째 지우고, 웹서버의 alias·location 설정과 docker-compose의 phpmyadmin·adminer 서비스도 함께 뺀다. 웹서버 설정 문법을 확인해 reload한 뒤 휴대폰 데이터 같은 바깥 망에서 자가진단 1단계를 다시 돌려 404가 나오는지 본다.
2꼭 써야 하면 접속 IP를 제한한다. 사무실이나 VPN의 고정 IP만 허용하고(위 설정), Docker라면 compose의 포트를 127.0.0.1로만 게시한 뒤 SSH 터널(ssh -L 8080:127.0.0.1:8080 계정@서버)로 접속한다. Docker 문서는 게시 옵션에 로컬호스트 주소를 붙이면 Docker 호스트만 해당 포트에 접근할 수 있다고 설명하되, 28.0.0 미만 버전은 같은 네트워크 구간의 다른 호스트가 닿을 수 있다고 함께 안내하므로 버전을 확인해 갱신한다.
3웹서버 단계의 별도 인증을 한 겹 더 둔다. phpMyAdmin 공식 문서는 사용자가 MySQL 계정을 넘기기 전에 먼저 인증받는 인증 프록시 뒤에 두라고 안내하고, Adminer 공식 사이트와 벤더 어드바이저리도 웹서버 비밀번호와 OTP 플러그인을 언급한다. DB 계정은 도구 전용으로 최소 권한만 주고, AllowRoot를 false로 두어 root 접속을 막고, AllowNoPassword는 false로 유지하며, phpMyAdmin 계정에는 2단계 인증을 켠다. 접속은 HTTPS로만 받는다.
4최신 버전을 유지한다. phpMyAdmin은 공식 문서대로 보안 공지가 나오면 올리고 setup·test 폴더를 지운다. 2025년 1월 공지 3건(XSS 2건, glibc/iconv 1건)은 5.x의 5.2.2 미만이 대상이었고, glibc/iconv 건은 기본 설정에서는 영향이 없다고 적혀 있다. Adminer는 4.7.9 미만이 CVE-2021-21311 범위이고 벤더가 완화책으로 단일 드라이버 파일(adminer-mysql.php 등) 사용을 안내했으니, 필요할 때만 공식 사이트에서 최신 파일을 받아 올리고 끝나면 지운다.
5이미 열려 있었다면 접속됐다고 가정하고 확인한다. 웹서버 접근 로그에서 해당 경로의 200 응답과 요청 IP를 뽑고(grep -iE 'phpmyadmin|adminer' access.log), phpMyAdmin의 실패한 로그인은 웹서버 로그가 아니라 syslog에 남을 수 있으니 함께 본다. phpMyAdmin 문서는 이 로그를 fail2ban 같은 도구에 연결해 대입 시도를 차단하는 방법도 안내한다. DB 계정 비밀번호를 교체하고, SELECT user, host FROM mysql.user; 로 낯선 계정이 생기지 않았는지 보고, DB에 저장된 API 키와 토큰이 있으면 교체 대상으로 올린다.
6재발을 막는 규칙을 배포 전에 건다. CI에서 git ls-files | grep -iE 'adminer|phpmyadmin' 결과가 비어 있는지와 compose 파일의 포트가 127.0.0.1로만 게시되는지 확인하고, 배포 뒤에는 바깥 망에서 자가진단 1단계를 주기적으로 돌린다. 두 경로 밖의 이름이나 포트는 외부 점검 도구가 보지 못하니 이 목록으로 사람이 챙긴다.

지금 해야 할 것

실무자: 자가진단 1~4단계를 바깥 망에서 먼저 돌리고 찾은 도구는 삭제부터 한다. CodeScan 무료 스캔은 입력한 주소 아래 /phpmyadmin/ 과 /adminer.php 두 경로만 확인하고, 다른 이름·포트·서브도메인에 둔 도구와 버전·설정은 보지 않는다. 무료 결과 화면은 위험 항목의 제목과 위치를 가린다.
CISO·임원: 관리 도구를 포함해 웹·API 의 외부 노출과 접근 제어를 함께 진단하려면 SENTRIX 보안 상담으로.

관련 글

내 사이트도 점검해보세요

URL 입력, 결과 링크는 메일로 받는 무료 점검. 회원가입·카드 불필요.

AI 코딩 보안 체크리스트 받기

AI 코딩 보안 체크리스트(PDF)를 무료로 받아보세요.

배포·설정을 바꾼 뒤에는 같은 주소를 다시 점검하세요. 수정 후 재점검 순서와 일정 저장 · 자동 점검이 필요하면 Continuous의 범위와 체험 조건을 확인하세요.
이런 글을 계속 받아보려면 뉴스레터 구독 · RSS 피드