fix(security): Address remaining scanning alerts

Tighten the remaining high-signal security findings by switching the todo example to a standard Express rate limiter, removing sensitive metadata from boilerplate logging, and replacing fragile HTML tag filtering with parser-based conversion.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sickn33
2026-03-18 18:15:28 +01:00
parent 6114c38cda
commit 344854e9e5
7 changed files with 160 additions and 66 deletions

View File

@@ -403,6 +403,19 @@ def redact_findings_for_report(findings: list[dict]) -> list[dict]:
return redacted
def build_safe_scanner_summaries(scanner_summaries: dict[str, dict]) -> dict[str, dict]:
"""Return scanner summaries with primitive numeric values only."""
safe_summaries: dict[str, dict] = {}
for scanner_name, summary in scanner_summaries.items():
safe_summaries[scanner_name] = {
"findings": int(summary.get("findings", 0)),
"score": float(summary.get("score", 0)),
}
return safe_summaries
def format_text_report(
target: str,
domain_scores: dict[str, float],
@@ -608,6 +621,9 @@ def run_score(
all_findings_raw = secrets_findings + dep_findings + inj_findings + quick_findings
all_findings = _deduplicate_findings(all_findings_raw)
total_findings = len(all_findings)
safe_findings = redact_findings_for_report(all_findings)
safe_total_findings = len(safe_findings)
safe_scanner_summaries = build_safe_scanner_summaries(scanner_summaries)
logger.info(
"Aggregated %d raw findings -> %d unique (deduplicated)",
@@ -657,8 +673,8 @@ def run_score(
result=f"final_score={final_score}, verdict={verdict['label']}",
details={
"domain_scores": domain_scores,
"total_findings": total_findings,
"scanner_summaries": scanner_summaries,
"total_findings": safe_total_findings,
"scanner_summaries": safe_scanner_summaries,
"duration_seconds": round(elapsed, 3),
},
)
@@ -671,9 +687,9 @@ def run_score(
domain_scores=domain_scores,
final_score=final_score,
verdict=verdict,
scanner_summaries=scanner_summaries,
scanner_summaries=safe_scanner_summaries,
all_findings=all_findings,
total_findings=total_findings,
total_findings=safe_total_findings,
elapsed=elapsed,
)
@@ -685,8 +701,8 @@ def run_score(
domain_scores=domain_scores,
final_score=final_score,
verdict=verdict,
scanner_summaries=scanner_summaries,
total_findings=total_findings,
scanner_summaries=safe_scanner_summaries,
total_findings=safe_total_findings,
elapsed=elapsed,
))

View File

@@ -95,7 +95,7 @@ def wait_for_oauth_code() -> Optional[str]:
"""Inicia servidor local e espera pelo código de autorização."""
server = HTTPServer(("localhost", OAUTH_REDIRECT_PORT), OAuthCallbackHandler)
server.timeout = 120 # 2 minutos
print(f"Aguardando autorização em http://localhost:{OAUTH_REDIRECT_PORT}/callback ...")
print("Aguardando autorização no callback OAuth local...")
print("(Timeout: 2 minutos)\n")
while OAuthCallbackHandler.authorization_code is None:
@@ -297,8 +297,7 @@ async def setup() -> None:
)
print("\nAbrindo browser para autorização...")
print(f"App ID em uso: {_mask_secret(app_id)}")
print("A URL de autorização não será exibida para evitar vazamento de credenciais.\n")
print("A URL de autorização e o App ID não serão exibidos para evitar vazamento de credenciais.\n")
webbrowser.open(auth_url)
# Esperar callback

View File

@@ -10,7 +10,8 @@
"dependencies": {
"better-sqlite3": "^12.8.0",
"cors": "^2.8.5",
"express": "^4.18.2"
"express": "^4.18.2",
"express-rate-limit": "^8.3.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
@@ -685,6 +686,24 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -885,6 +904,15 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@@ -11,7 +11,8 @@
"dependencies": {
"better-sqlite3": "^12.8.0",
"cors": "^2.8.5",
"express": "^4.18.2"
"express": "^4.18.2",
"express-rate-limit": "^8.3.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",

View File

@@ -1,11 +1,11 @@
import { Router, Request, Response } from 'express';
import { rateLimit } from 'express-rate-limit';
import { getDatabase } from '../db';
import { ApiResponse, Todo } from '../types/index';
const router = Router();
const WINDOW_MS = 60_000;
const MAX_REQUESTS_PER_WINDOW = 60;
const requestCounts = new Map<string, { count: number; resetAt: number }>();
const db = getDatabase();
type TodoRow = Omit<Todo, 'completed'> & { completed: number };
@@ -17,27 +17,15 @@ function toTodo(row: TodoRow): Todo {
};
}
function rateLimit(req: Request, res: Response, next: () => void): void {
const now = Date.now();
const clientKey = req.ip || 'unknown';
const entry = requestCounts.get(clientKey);
if (!entry || entry.resetAt <= now) {
requestCounts.set(clientKey, { count: 1, resetAt: now + WINDOW_MS });
next();
return;
}
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
res.status(429).json({ error: 'Too many requests. Please retry later.' });
return;
}
entry.count += 1;
next();
}
router.use(rateLimit);
router.use(
rateLimit({
windowMs: WINDOW_MS,
limit: MAX_REQUESTS_PER_WINDOW,
standardHeaders: 'draft-8',
legacyHeaders: false,
message: { error: 'Too many requests. Please retry later.' },
})
);
// GET /api/todos - Retrieve all todos
router.get('/todos', (_req: Request, res: Response): void => {

View File

@@ -67,7 +67,7 @@ async def handle_incoming_message(message: dict) -> None:
from_number = message["from"]
content = extract_message_content(message)
logger.info("Received WhatsApp message type=%s message_id=%s", content["type"], message["id"])
logger.info("Received WhatsApp message")
# Mark as read
await whatsapp.mark_as_read(message["id"])
@@ -84,7 +84,7 @@ async def handle_incoming_message(message: dict) -> None:
await whatsapp.send_text(from_number, "Recebi sua escolha com sucesso.")
case "image" | "document" | "video" | "audio":
await whatsapp.send_text(from_number, f"Recebi sua midia ({content['type']}).")
await whatsapp.send_text(from_number, "Recebi sua midia com sucesso.")
case _:
await whatsapp.send_text(from_number, "Desculpe, nao entendi. Como posso ajudar?")

View File

@@ -11,10 +11,99 @@ import re
import sys
import urllib.request
import urllib.error
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
from typing import Dict, Optional, Tuple
from urllib.parse import urlparse, urljoin
class MarkdownHTMLParser(HTMLParser):
"""Convert a constrained subset of HTML into markdown without regex tag stripping."""
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self._parts: list[str] = []
self._ignored_tag: Optional[str] = None
self._ignored_depth = 0
self._current_link: Optional[str] = None
self._list_depth = 0
self._in_pre = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
if self._ignored_tag:
if tag == self._ignored_tag:
self._ignored_depth += 1
return
if tag in {"script", "style"}:
self._ignored_tag = tag
self._ignored_depth = 1
return
attrs_dict = dict(attrs)
if tag in {"article", "main", "div", "section"}:
self._append("\n")
elif tag == "br":
self._append("\n")
elif tag == "p":
self._append("\n\n")
elif tag in {"h1", "h2", "h3"}:
prefix = {"h1": "# ", "h2": "## ", "h3": "### "}[tag]
self._append(f"\n\n{prefix}")
elif tag in {"ul", "ol"}:
self._list_depth += 1
self._append("\n")
elif tag == "li":
indent = " " * max(0, self._list_depth - 1)
self._append(f"\n{indent}- ")
elif tag == "a":
self._current_link = attrs_dict.get("href")
self._append("[")
elif tag == "pre":
self._in_pre = True
self._append("\n\n```\n")
elif tag == "code" and not self._in_pre:
self._append("`")
def handle_endtag(self, tag: str) -> None:
if self._ignored_tag:
if tag == self._ignored_tag:
self._ignored_depth -= 1
if self._ignored_depth == 0:
self._ignored_tag = None
return
if tag in {"h1", "h2", "h3", "p"}:
self._append("\n")
elif tag in {"ul", "ol"}:
self._list_depth = max(0, self._list_depth - 1)
self._append("\n")
elif tag == "a":
href = self._current_link or ""
self._append(f"]({href})")
self._current_link = None
elif tag == "pre":
self._in_pre = False
self._append("\n```\n")
elif tag == "code" and not self._in_pre:
self._append("`")
def handle_data(self, data: str) -> None:
if self._ignored_tag or not data:
return
self._append(unescape(data))
def get_markdown(self) -> str:
markdown = "".join(self._parts)
markdown = re.sub(r"\n{3,}", "\n\n", markdown)
return markdown.strip()
def _append(self, text: str) -> None:
if text:
self._parts.append(text)
def parse_frontmatter(content: str) -> Optional[Dict]:
"""Parse YAML frontmatter."""
fm_match = re.search(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
@@ -128,37 +217,10 @@ def extract_markdown_from_html(html_content: str) -> Optional[str]:
def convert_html_to_markdown(html: str) -> str:
"""Basic HTML to markdown conversion."""
# Remove scripts and styles
html = re.sub(r'<script\b[^>]*>.*?</script\s*>', '', html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r'<style\b[^>]*>.*?</style\s*>', '', html, flags=re.DOTALL | re.IGNORECASE)
# Headings
html = re.sub(r'<h1[^>]*>(.*?)</h1>', r'# \1', html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r'<h2[^>]*>(.*?)</h2>', r'## \1', html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r'<h3[^>]*>(.*?)</h3>', r'### \1', html, flags=re.DOTALL | re.IGNORECASE)
# Code blocks
html = re.sub(r'<pre[^>]*><code[^>]*>(.*?)</code></pre>', r'```\n\1\n```', html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r'<code[^>]*>(.*?)</code>', r'`\1`', html, flags=re.DOTALL | re.IGNORECASE)
# Links
html = re.sub(r'<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>', r'[\2](\1)', html, flags=re.DOTALL | re.IGNORECASE)
# Lists
html = re.sub(r'<li[^>]*>(.*?)</li>', r'- \1', html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r'<ul[^>]*>|</ul>|<ol[^>]*>|</ol>', '', html, flags=re.IGNORECASE)
# Paragraphs
html = re.sub(r'<p[^>]*>(.*?)</p>', r'\1\n\n', html, flags=re.DOTALL | re.IGNORECASE)
# Remove remaining HTML tags
html = re.sub(r'<[^>]+>', '', html)
# Clean up whitespace
html = re.sub(r'\n{3,}', '\n\n', html)
html = html.strip()
return html
parser = MarkdownHTMLParser()
parser.feed(html)
parser.close()
return parser.get_markdown()
def create_minimal_markdown(metadata: Dict, source_url: str) -> str:
"""Create minimal markdown content from metadata."""