제미나이 이용해서 웹 보안 점검 도구 만들어봄
코드는
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>웹 취약점 스캐너 v3.1</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap');
:root {
--accent-color: #0d6efd;
--ok-color: #198754;
--vuln-color: #dc3545;
--info-color: #0dcaf0;
--bg-color: #f8f9fa;
--text-color: #212529;
--border-color: #dee2e6;
--card-bg: #ffffff;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
padding: 40px 20px;
margin: 0;
font-size: 16px;
}
.container {
width: 100%;
max-width: 800px;
margin: auto;
}
h1 {
text-align: center;
color: var(--text-color);
margin-bottom: 40px;
font-weight: 700;
}
#controls {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
#urlInput {
flex-grow: 1;
border: 1px solid var(--border-color);
padding: 12px 16px;
border-radius: 8px;
font-family: inherit;
font-size: 1em;
transition: border-color 0.2s, box-shadow 0.2s;
}
#urlInput:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.25);
}
#checkBtn {
background-color: var(--accent-color);
border: none;
color: white;
padding: 12px 24px;
font-family: inherit;
font-weight: 500;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.2s;
}
#checkBtn:hover { background-color: #0b5ed7; }
#checkBtn:disabled { background-color: #6c757d; cursor: not-allowed; }
#results-container {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
#results-container.visible { opacity: 1; }
.log-item {
margin-bottom: 12px;
display: flex;
align-items: center;
}
.log-prefix {
font-weight: 700;
margin-right: 10px;
min-width: 25px;
}
.ok { color: var(--ok-color); }
.vuln { color: var(--vuln-color); }
.info { color: var(--info-color); }
.summary-box {
border-top: 1px solid var(--border-color);
padding-top: 30px;
margin-top: 30px;
}
.summary-box h3 { margin-top: 0; }
.score-display {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
font-size: 1.2em;
margin-bottom: 10px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--ok-color);
border-radius: 10px;
transition: width 1s ease-in-out;
}
</style>
</head>
<body>
<div class="container">
<h1>웹 취약점 스캐너 v3.1</h1>
<div id="controls">
<input type="url" id="urlInput" placeholder="https://example.com">
<button id="checkBtn">점검 시작</button>
</div>
<div id="results-container">
<div id="results">
<p>점검할 URL을 입력하고 '점검 시작' 버튼을 눌러주세요.</p>
</div>
</div>
</div>
<script>
const PROXY_URL = 'https://corsproxy.io/?';
const urlInput = document.getElementById('urlInput');
const checkBtn = document.getElementById('checkBtn');
const resultsContainer = document.getElementById('results-container');
const resultsDiv = document.getElementById('results');
checkBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) return;
checkBtn.disabled = true;
checkBtn.textContent = '점검 중...';
resultsContainer.classList.remove('visible');
try {
const response = await fetch(`${PROXY_URL}${encodeURIComponent(url)}`);
if (!response.ok) throw new Error(`서버가 응답하지 않습니다 (상태: ${response.status})`);
const headers = response.headers;
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
let score = 100;
let resultsHTML = `<h3>점검 로그: ${escapeHTML(url)}</h3>`;
const checks = [
checkHttps(url),
checkHttpHeaders(headers),
checkCookieSecurity(headers),
checkMixedContent(doc),
checkInfoLeakage(html),
checkDangerousPatterns(doc),
];
checks.forEach(res => {
score -= res.deduction;
resultsHTML += res.log;
});
score = Math.max(0, score);
resultsHTML += generateSummary(score);
resultsDiv.innerHTML = resultsHTML;
// 점수 프로그레스 바 채우기
const progressFill = document.querySelector('.progress-fill');
if(progressFill) {
setTimeout(() => { progressFill.style.width = score + '%'; }, 100);
}
} catch (e) {
resultsDiv.innerHTML = `<p class="vuln"><b>점검 실패:</b> ${escapeHTML(e.message)}</p><p>프록시 서버 문제, 네트워크, 또는 잘못된 URL일 수 있습니다.</p>`;
} finally {
resultsContainer.classList.add('visible');
checkBtn.disabled = false;
checkBtn.textContent = '점검 시작';
}
});
function generateSummary(score) {
let grade = 'F';
let color = 'var(--vuln-color)';
if (score >= 95) { grade = 'S'; color = 'var(--ok-color)'; }
else if (score >= 90) { grade = 'A'; color = 'var(--ok-color)'; }
else if (score >= 80) { grade = 'B'; color = '#fd7e14'; }
else if (score >= 70) { grade = 'C'; color = '#ffc107'; }
return `
<div class="summary-box">
<h3>종합 보안 점수</h3>
<div class="score-display">
<span>등급: ${grade}</span>
<span style="color: ${color};">${score} / 100</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0%; background-color: ${color};"></div>
</div>
</div>`;
}
function escapeHTML(str) { return str.replace(/[&<>"']/g, match => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[match])); }
function createLog(cssClass, prefix, text) {
return `<div class="log-item ${cssClass}"><span class="log-prefix">${prefix}</span>${text}</div>`;
}
// 각 점검 함수는 {log: "...", deduction: 점수} 형태의 객체를 반환
function checkHttps(url) {
let deduction = 0;
let log = url.startsWith('https://')
? createLog('ok', '[+]', 'HTTPS 프로토콜을 사용합니다.')
: (deduction = 30, createLog('vuln', '[-]', '<b>[치명적]</b> 암호화되지 않은 HTTP 연결이 사용 중입니다.'));
return { log, deduction };
}
function checkHttpHeaders(headers) {
let log = '';
let deduction = 0;
const checks = {
'content-security-policy': 15, 'strict-transport-security': 10,
'x-frame-options': 8, 'x-content-type-options': 5
};
Object.entries(checks).forEach(([header, weight]) => {
if (headers.has(header)) {
log += createLog('ok', '[+]', `<b>${header}</b> 헤더가 설정되었습니다.`);
} else {
log += createLog('vuln', '[-]', `<b>${header}</b> 헤더가 누락되었습니다.`);
deduction += weight;
}
});
if (headers.has('server') || headers.has('x-powered-by')) {
log += createLog('vuln', '[-]', '서버 버전 정보가 노출되었습니다.');
deduction += 5;
}
return { log, deduction };
}
function checkCookieSecurity(headers) {
const setCookieHeader = headers.get('set-cookie');
if (!setCookieHeader) return { log: createLog('info', '[*]', '페이지에서 설정하는 쿠키가 없습니다.'), deduction: 0 };
const cookies = setCookieHeader.split(',');
const insecureCookies = cookies.filter(c => !(/; *secure/i.test(c) && /; *httponly/i.test(c)));
let log = insecureCookies.length > 0
? createLog('vuln', '[-]', `<b>${insecureCookies.length}개</b>의 쿠키에 Secure 또는 HttpOnly 속성이 누락되었습니다.`)
: createLog('ok', '[+]', '모든 쿠키에 보안 속성이 올바르게 적용되었습니다.');
return { log, deduction: insecureCookies.length > 0 ? 10 : 0 };
}
function checkMixedContent(doc) {
const insecureElements = doc.querySelectorAll('img[src^="http://"], script[src^="http://"], link[href^="http://"]');
let log = insecureElements.length > 0
? createLog('vuln', '[-]', `<b>${insecureElements.length}개</b>의 혼합 콘텐츠(Mixed Content)가 발견되었습니다.`)
: createLog('ok', '[+]', '혼합 콘텐츠가 발견되지 않았습니다.');
return { log, deduction: insecureElements.length > 0 ? 10 : 0 };
}
function checkInfoLeakage(html) {
const emailRegex = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi;
const ipRegex = /\b(192\.168|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))\.\d{1,3}\.\d{1,3}\b/g;
let deduction = 0;
let log = '';
if (emailRegex.test(html)) {
log += createLog('vuln', '[-]', '소스 코드 내에 이메일 주소가 노출되었습니다.');
deduction = 5;
}
if (ipRegex.test(html)) {
log += createLog('vuln', '[-]', '소스 코드 내에 내부 IP 주소가 노출되었습니다.');
deduction = 5;
}
if (deduction === 0) {
log = createLog('ok', '[+]', 'HTML 소스 코드에서 민감 정보가 발견되지 않았습니다.');
}
return { log, deduction };
}
function checkDangerousPatterns(doc) {
const scripts = doc.querySelectorAll('script');
const dangerousPatterns = ['.innerHTML', 'document.write', 'eval('];
let found = false;
scripts.forEach(script => {
const code = script.textContent;
if (dangerousPatterns.some(pattern => code.includes(pattern))) {
found = true;
}
});
let log = found
? createLog('vuln', '[-]', 'DOM XSS로 이어질 수 있는 위험한 코드 패턴이 발견되었습니다.')
: createLog('ok', '[+]', '스크립트에서 잠재적으로 위험한 패턴이 발견되지 않았습니다.');
return { log, deduction: found ? 10 : 0 };
}
</script>
</body>
</html>
이렇게 나옴 결과는?

이렇게 나옴 ㄷㄷ
와중에 네이버 실화냐;;;; 심지어 구글은 분석조차 안됨. 자동화 접근 막아놓은듯....
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.