디웹시 Blog 검색

그누보드 보안 점검 사이트를 하루 만에 만들어 배포한 기록

그누보드 보안 점검을 주소 하나로 끝내는 무료 진단기를 디웹시에서 하루 만에 만들어 배포했습니다. 어그로 사이트 분석에서 시작해 오탐, 호스팅 차단까지 겪은 과정을 적었어요.

수정

공유

아침에 누가 보낸 링크 하나를 열어봤어요. 인스타그램 프로필을 누가 봤는지 알려준다는 사이트였고, 주소 끝에 추천인 코드가 붙어 있었습니다. 기술적으로 불가능한 기능을 파는 곳이었는데, 저는 그 사이트가 어떻게 돈을 버는지가 더 궁금했어요. 그 궁금증이 저녁에는 그누보드 보안 점검 서비스 배포로 끝났습니다. 이 글은 그 하루의 기록이에요.

저는 디웹시라는 이름으로 혼자 일하는 풀스택 개발자고, 그누보드 사이트의 PHP를 최신 버전으로 올려주는 외주를 팔고 있습니다. 문제는 사장님들이 자기 사이트가 위험한지 자체를 모른다는 거였어요. “홈페이지 만든 업체랑 연락이 끊겼다”는 말을 정말 자주 듣거든요. 위험한 줄 모르니 고칠 생각도 안 하고, 저한테까지 올 일도 없는 구조였습니다.

어그로 사이트에서 가져온 건 훅 하나뿐

그 인스타 방문자 사이트의 수익 구조는 단순했어요. 불안을 자극하는 문구로 유입시키고, 결과를 보려면 선충전하게 하고, 결제한 사람한테 추천 코드를 쥐여줘서 퍼뜨리게 합니다. 보여주는 데이터는 애초에 만들 수 없는 거니까 원가가 0이에요. 나도 이런 거라도 하나 해야 하나 싶은 생각이 솔직히 들었습니다.

근데 그 모델은 못 따라가요. 핵심 가치가 거짓이면 환불 분쟁이 쌓이고, 사업자 이름 걸고 하는 사람이 할 게 아니죠. 대신 그 사이트가 잘하는 세 가지는 가져올 수 있겠더라고요. 감정을 건드리는 문구, 아이디 하나만 넣으면 결과가 나오는 진입 장벽, 결과 자체가 공유되는 구조. 훅은 어그로답게, 결과물은 진짜로. 이게 그날의 설계 원칙이 됐습니다.

그래서 고른 게 홈페이지 보안 진단이었어요. 도메인을 넣으면 A부터 F까지 등급이 나오고, 등급이 낮으면 제가 운영 중인 PHP 업데이트 서비스로 이어지는 흐름입니다. 이미 돈을 받고 있는 상품으로 연결되는 유일한 훅이었고, 타깃이 제 외주 고객과 같아서 잘못 만들어도 영업 자료로는 남는다는 계산이었습니다.

디웹시 보안 등급 진단 랜딩 페이지

시장조사 30분, 국내엔 셀프서브가 없었다

만들기 전에 경쟁 서비스를 찾아봤는데, 국내에는 주소 넣고 바로 등급 받는 형태가 없었어요. 상공회의소 무료 점검은 신청서를 내야 하고, 보안 업체 진단은 B2B 컨설팅이고, 크몽에서 파는 취약점 진단은 사람이 며칠 걸려서 해주는 거라 수십만 원이었습니다. 해외에는 securityheaders.com이나 Sucuri SiteCheck처럼 무료로 등급을 매기고 유료 서비스로 넘기는 모델이 이미 자리 잡혀 있는데, 전부 영어고 그누보드는 안 잡아요.

수요 근거도 명확했습니다. php.net의 지원 종료 목록을 보면 PHP 7.4는 2022년 11월 28일, 8.0은 2023년 11월 26일, 8.1은 2025년 12월 31일에 지원이 끝났어요. 2026년 11월 기준으로 8.1 이하는 전부 보안 패치가 안 나옵니다. 그런데 국내 그누보드 사이트 상당수가 아직 7.4에 있어요. 그누보드 자체도 공식 GitHub 저장소에서 2026년 11월 2일에 5.6.46이 나왔을 정도로 패치가 계속되고 있는데, 5.4대에 멈춘 사이트가 흔합니다.

경쟁자 없음, 수요 있음, 한국어, 그누보드 특화. 이 네 가지가 맞아떨어져서 바로 설계로 넘어갔습니다.

그누보드 보안 점검 엔진이 실제로 보는 것

원칙은 공개된 것만 조회한다는 거예요. 반대로 진단기 자체가 공격 도구가 되지 않게 막는 것도 같은 비중으로 챙겼습니다. 사용자가 넣은 주소를 서버가 대신 요청하는 구조라, 누가 127.0.0.1이나 사설 IP를 넣으면 제 서버 내부의 DB 포트를 찌르게 될 수 있거든요. 그래서 도메인을 IP로 풀어본 뒤 사설 대역이면 거부하고, 리다이렉트 대상도 같은 검사를 거칩니다. 테스트 중에 제 사이트로 리다이렉트되는 주소가 검사를 통과하는 구멍이 하나 나와서 그것도 막았어요. 디웹시 서버 한 대에 다른 서비스 여덟 개가 같이 돌고 있어서 이건 타협할 수 없었습니다. 로그인 시도 없고, 파라미터 주입 없고, 요청은 20개 이내, 타임아웃 6초, 같은 도메인은 한 시간에 한 번, 하루 다섯 번까지만. 이 선을 지키면 브라우저가 하는 일과 다를 게 없습니다. 비개발자 입장에서 말하면, 누구나 주소창에 쳐서 볼 수 있는 것만 본다는 뜻이에요.

확인 항목은 SSL 인증서 만료일과 HTTPS 강제 여부, 서버와 PHP 버전 노출, 그누보드 버전, 관리자 페이지와 설치 폴더 노출, data 폴더 목록 노출, .env나 .git 같은 민감 파일 노출, 보안 헤더 네 종류입니다. 각각 감점을 두고 100점에서 빼서 등급을 냅니다.

처음 설계할 때 저는 PHP 버전을 응답 헤더에서 읽으면 된다고 생각했는데, 제가 직접 “헤더에 PHP 버전 거의 안 뜰 텐데”라고 짚었어요. 실제로 카페24나 가비아 같은 호스팅은 기본으로 가려놓습니다. 그래서 PHP 버전은 보너스로 두고, 뼈대는 그누보드 버전으로 잡았습니다. 그누보드 5.4 미만은 PHP 8에서 안 돌아가니까 구버전이면 PHP도 구버전이라고 역추정할 수 있거든요.

그누보드 버전은 어떻게 알아내느냐가 핵심이었어요. 공식 저장소의 태그 258개를 전부 받아서 js/common.js, css/default.css, js/wrest.js 세 파일의 해시를 태그별로 뽑았습니다. common.js만 31개 구간으로 갈리더라고요. 사이트에서 그 파일을 받아 해시를 비교하면 “5.4.6에서 5.4.22 사이, 마지막 릴리스 2022년 1월” 수준으로 특정됩니다. 해시가 안 맞으면 HTML에 박힌 common.js?ver= 값으로 한 번 더 시도하고, 그것도 안 되면 코어를 손댄 사이트로 보고 점검 권장으로 떨어뜨립니다.

이 폴백은 사실 제가 한 번 틀렸다가 되찾은 거예요. 처음에 “그누보드는 common.js에 날짜 버전 상수를 붙인다”고 했다가, master 브랜치 소스를 보니 그런 상수가 없길래 철회했습니다. 근데 실제 사이트에서 ?ver=2304171 같은 값이 나오더라고요. 태그를 거슬러 올라가 보니 5.4부터 5.6.35까지는 G5_JS_VER라는 상수가 있었고, 2026년 8월 커밋에서 파일 수정 시각 방식으로 바뀐 거였습니다. 최신 브랜치 하나만 보고 단정하면 안 된다는 걸 엔진 만들다가 다시 배웠어요. 그누보드 보안 점검처럼 버전을 다루는 작업은 릴리스 역사 전체를 봐야 합니다.

터미널에서 그누보드 사이트를 진단해 버전 구간이 추정된 결과

오탐 두 번, 그리고 서버에서만 틀리는 등급

엔진을 돌리자마자 오탐이 나왔어요. 처음엔 깃허브 같은 사이트에서도 관리자 페이지가 뚫렸다고 나왔는데, 없는 경로에도 200을 돌려주는 soft 404 때문이었습니다. 그래서 진단 시작 때 일부러 존재할 리 없는 경로를 하나 찔러서, 그게 200이면 경로 판정을 보류하는 기준선을 넣었어요.

그다음이 설치 폴더였습니다. 테스트한 그누보드 사이트 네 곳 전부에서 설치 폴더가 뚫렸다고 나온 겁니다. 네 곳이 다 뚫렸을 리는 없죠. 진짜로 그렇다면 그누보드 보안 점검이라는 게 애초에 의미가 없을 테니까요. 소스를 보니 그누보드는 설치가 끝난 뒤 /install/에 접근하면 “이미 설치되어 있습니다” 안내를 정상 응답으로 돌려줘요. 제 조건이 그 안내 페이지에 반응한 거였습니다. 완전히 무해한 건 아니에요. 그 안내 페이지에 서버 절대경로가 그대로 찍히거든요. 그래서 치명이 아니라 경고 5점 감점으로 바꿨습니다.

점수 가중치도 그때 한 번 손봤어요. 첫 결과가 네 곳 중 세 곳이 F였는데, 사이트 상태가 나쁜 건 맞지만 전부 F면 공유 카드가 다 똑같아서 훅이 죽습니다. 원인은 보안 헤더 네 종 15점에 HTTPS 리다이렉트 10점이 더해져 그누보드 기본 설치만으로 25점이 깎이는 구조였어요. 헤더는 없다고 당장 털리는 게 아니라서 비중을 10점으로 내리고, 대신 구버전과 지원 종료 PHP, 파일 노출처럼 실제로 위험한 항목에 무게를 몰았습니다. 그러고 나니 D, D, F, C로 갈렸어요. 등급이 갈려야 사람들이 자기 걸 넣어보고 싶어집니다.

두 번째는 배포 직전에 터졌어요. 제 맥에서는 D가 나오던 사이트가 운영 서버에서 돌리니 B가 나왔습니다. 이대로 공개하면 취약한 사이트에 좋은 등급을 주는 거라 바로 멈췄어요. 원인은 호스팅이었습니다. 일부 국내 호스팅이 데이터센터 IP에는 자바스크립트 챌린지 화면을 200 상태로 보내고 있었고, 엔진은 그걸 정상 페이지로 읽어서 그누보드도 PHP도 못 찾고 경로 여덟 개가 전부 “차단됨”으로 양호 판정이 난 거예요.

여기서 선택지가 둘이었습니다. 챌린지를 그대로 막힘으로 처리하거나, 국내 가정 회선에서 진단만 따로 돌리거나. 판단 근거를 만들려고 그누보드 사이트 30곳을 모아 운영 서버와 제 맥에서 각각 루트만 한 번씩 요청해봤습니다.

분류운영 서버집 회선
정상 응답21곳 (70%)27곳 (90%)
JS 챌린지2곳0곳
403·429·5033곳1곳
타임아웃·연결 실패4곳2곳

서버에서만 막히는 사이트가 30곳 중 6곳, 20%였어요. 호스팅사 한 곳에 몰린 것도 아니었습니다. 카페24와 가비아 일부의 챌린지, Cloudflare의 데이터센터 IP 거부, 국내 IDC의 타임아웃이 섞여 있었어요. 워커를 따로 만들어도 Cloudflare 403은 못 풀고, 20%를 위해 가정 회선 서버를 운영하는 건 수지가 안 맞았습니다.

자동 진단이 차단된 사이트의 결과 화면

우회하지 않기로 한 이유

그래서 막히면 막혔다고 보여주기로 했습니다. 루트 응답이 정상이 아니거나 챌린지 흔적이 있으면 점수 없이 “보안 장비가 자동 진단을 차단했습니다. 수동 점검으로 확인해드립니다”로 보내요. 어차피 수동 점검이 유료 서비스라 손해가 아니고, 오히려 전환 포인트입니다.

솔직히 여기서 한 번 선을 넘을 뻔했어요. 406을 돌려주는 호스팅이 세 곳 있어서, 브라우저가 보내는 헤더를 전부 흉내 내면 뚫리지 않을까 하고 시켜봤거든요. Claude Code가 그 요청을 거절했습니다. 사람이 보낸 것처럼 꾸며서 남의 방화벽을 피해 가는 구조는 챌린지를 푸는 것과 같다는 이유였어요. 맞는 말이라 봇임을 밝히는 UA로 되돌렸습니다. 진단기 주소를 UA에 적어두고, 막는 곳은 막는 대로 둡니다.

많은 분이 “보안 점검 툴이면 당연히 다 뚫어야 하는 거 아니냐”고 생각하시는데, 그건 침투 테스트고 그것도 소유자 동의가 전제예요. 공개 정보 진단은 브라우저가 볼 수 있는 것만 보는 게 맞고, 그 이상은 사이트 주인이 허락한 뒤에 하는 겁니다. 나중에 루트에 확인 파일을 올려 소유를 인증한 사이트만 전체 진단하는 방식을 붙일 생각이에요.

오늘 안에 배포한 과정

제 서버는 Nginx에 Astro와 React로 돌아가고 PHP는 없어요. 처음에 PHP로 짜자고 했다가 제가 “내 서버에 PHP가 있어? 없을 텐데”라고 바로잡았습니다. 엔진이 이미 파이썬으로 검증된 상태라 FastAPI를 얹어 PM2로 띄우고, 프론트는 기존 Astro 사이트에 페이지를 추가하는 걸로 정리했어요. 포팅이 사라지니 시간이 확 줄었습니다.

배포 전에 랜딩 문구도 한 번 걷어냈습니다. 처음 버전에는 “HSTS, X-Frame-Options” 같은 약어와 “주소 20곳 이내만 확인합니다” 같은 내부 동작 설명이 그대로 있었어요. 사장님 입장에서는 불안만 주고 이해는 안 되는 문장들이라, 확인 항목을 “보안 인증서가 정상인지”, “그누보드와 PHP가 지원 종료된 구버전인지” 수준의 다섯 줄로 바꾸고 나머지는 지웠습니다. 소비자가 알 필요 없는 문구는 없는 게 낫더라고요.

설계와 검증은 Claude 웹에서, 서버 조사와 구현과 배포는 Claude Code가 맡았습니다. 서버 현황을 먼저 읽기 전용으로 조사시켜서 스택과 nginx 구조를 파악한 다음, 그 결과를 바탕으로 스펙을 쓰고 넘기는 순서였어요. 추측으로 스펙을 쓰면 두 번 틀립니다. 제가 그날 실제로 두 번 틀렸고요.

결과 페이지마다 등급이 크게 들어간 OG 이미지를 Pillow로 만들어서, 카톡에 링크만 던져도 등급이 보이게 했습니다. 카카오 공유 SDK는 안 붙였어요. 링크만 붙여도 OG 카드가 뜨는데 앱 등록이랑 키 관리까지 할 이유가 없었습니다. 모바일은 운영체제 공유 시트 하나로 끝나요.

아침에 어그로 사이트 링크를 열어본 뒤 저녁에 운영 배포까지 하루였습니다. 배포 뒤 제가 몇 개 넣어보니 막힌 곳 빼고는 잘 나왔고, 결과는 디웹시 보안 등급 진단에서 바로 볼 수 있습니다. 등급이 C 이하면 그누보드 PHP 업데이트 서비스로 이어져요.

카카오톡에 공유된 보안 등급 결과 카드

남은 숙제와 다음 2주

지금 운영 서버에서 실제로 등급을 매길 수 있는 사이트는 30곳 중 16곳 정도예요. 나머지는 막힘 안내로 갑니다. 이 숫자가 아쉽지만, 틀린 등급을 내보내는 것보다는 낫다고 봐요. 그누보드 보안 점검 결과가 한 번이라도 틀리면 신뢰가 끝나거든요.

핑거프린트 테이블은 그누보드 릴리스가 나올 때마다 다시 뽑아야 해서 월 1회 재생성하는 절차를 README에 넣어뒀습니다. 그누보드 5.6.36부터는 ?ver= 값이 날짜 상수가 아니라 파일 수정 시각으로 바뀌어서, 버전 판정 방식도 릴리스마다 흔들릴 수 있어요. 이건 지켜봐야 합니다.

재진단 간격은 처음에 24시간으로 잡았다가 다음 날 1시간으로 줄였어요. sir.kr에 올린 글에 “수정하고 다시 체크하려면 하루 기다려야 하는 게 아쉽다”는 댓글이 달렸는데, 맞는 말이었습니다. 고치고 바로 확인이 안 되면 제작사 입장에서는 반쪽이고, “D였는데 고쳐서 A 됐다”를 보여주는 게 제일 좋은 홍보인데 그걸 제가 막고 있던 거예요. 하루 5회 상한은 남겨서 반복 요청으로 호스팅에 막히는 건 피했습니다.

배포 당일에 sir.kr과 스레드, 디스콰이엇에 글을 올렸고, 다음은 기존 고객들한테 등급을 들고 가보는 거예요. 콜드 메시지로 “당신 사이트 F입니다”는 안 합니다. 협박처럼 읽혀서 역효과예요. 측정에 쓴 30곳 주인한테도 연락하지 않습니다. 허락 없이 등급을 매겨놓고 찾아가는 건 제가 비판한 어그로 사이트와 다를 게 없으니까요. 먼저 들어온 분들한테만 결과를 보여주고, 2주 뒤에 전환이 됐는지 숫자를 이 블로그에 적겠습니다.

이어서