제미나이 이용해서 웹 보안 점검 도구 만들어봄
코드는
<!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">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">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/">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>
이렇게 나옴 결과는?
이렇게 나옴 ㄷㄷ
와중에 네이버 실화냐;;;; 심지어 구글은 분석조차 안됨. 자동화 접근 막아놓은듯....
)추가 아래 댓글에서 SGL 인젝션 확인 못하는거 얘기 나와서 완벽히 확인은 못하지만 위험도를 알 수 있도록 코드 수정해봄
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>웹 취약점 스캐너 v3.2 (SQLi 포함)</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.2 (SQLi 포함)</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 syncChecks = [
checkHttps(url),
checkHttpHeaders(headers),
checkCookieSecurity(headers),
checkMixedContent(doc),
checkInfoLeakage(html),
checkDangerousPatterns(doc),
];
syncChecks.forEach(res => {
score -= res.deduction;
resultsHTML += res.log;
});
// --- SQL 인젝션 (비동기) 점검 추가 ---
const sqlCheckResult = await checkSqlInjection(url);
score -= sqlCheckResult.deduction;
resultsHTML += sqlCheckResult.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 };
}
/**
* [신규] SQL 인젝션 취약점 점검 함수 (비동기)
* URL에 일반적인 SQL 인젝션 패턴(')을 추가하여 요청을 보낸 후,
* 응답 텍스트에 SQL 오류 관련 키워드가 포함되어 있는지 확인합니다.
* @param {string} baseUrl - 점검할 기본 URL
* @returns {Promise<{log: string, deduction: number}>} 점검 결과 객체
*/
async function checkSqlInjection(baseUrl) {
let log = '';
let deduction = 0;
const payload = "'"; // 가장 기본적인 SQL 인젝션 테스트 페이로드
const testUrl = `${baseUrl}?sql_test_param=${payload}`;
try {
const response = await fetch(`${PROXY_URL}${encodeURIComponent(testUrl)}`);
const responseText = await response.text();
// 일반적인 SQL 오류 메시지 패턴 (대소문자 무시)
const sqlErrorRegex = /SQL|syntax|error|ORA-|ODBC|JDBC|unclosed|quotation/i;
if (sqlErrorRegex.test(responseText)) {
deduction = 35; // 치명적인 취약점이므로 높은 점수 감점
log = createLog('vuln', '[-]', '<b>[치명적]</b> URL 파라미터에서 SQL 인젝션 취약점이 의심됩니다. 서버 오류 메시지가 노출되었습니다.');
} else {
log = createLog('ok', '[+]', '기본적인 SQL 인젝션 패턴에 대한 서버 오류가 발견되지 않았습니다.');
}
} catch (e) {
log = createLog('info', '[*]', 'SQL 인젝션 테스트 중 오류가 발생하여 점검을 건너뛰었습니다.');
}
return { log, deduction };
}
</script>
</body>
</html>
프롬프트
근데 보안헤더도 헤더지만 SQL 인젝션이나 이런건 못보지 않나. 그냥 보안 헤더 잘지키는가 이거 아님?
인젝션은 서버단에서 따로 막아야할듯
SGL인젝션 막은 버젼 만듬 코드 올릴
이거 내가 저장해놓고 두고두고써야겠네 프론트 엔드 개발할때 좋은듯
웹 린터 같은 역할인듯