Segurança — obsidian-mcp-secure
O
obsidian-mcp-securefoi construído com segurança como requisito de design, não afterthought. Cada controle aqui foi codado deliberadamente, com referência ao OWASP Top 10 aplicado ao contexto de um MCP server.
🎯 Threat model
Cenário-base
Um usuário roda o obsidian-mcp-secure localmente. O Claude Desktop (ou outro cliente MCP) envia chamadas via stdio. O servidor traduz pra requisições HTTP no plugin Local REST API do Obsidian, que vive em localhost:27123.
Atacantes considerados
| Vetor | Mitigação |
|---|---|
| Prompt injection malicioso via Claude que tenta ler arquivos fora do vault | A01 — path traversal bloqueado |
| Plugin malicioso de outro app tentando imitar o tráfego | A02 — API key obrigatória, validada via Bearer token |
| Conteúdo malicioso em nota que tenta executar código | A03 — sem eval, sem exec, sem shell |
| Payload gigante (DoS) | A04 — limite de 512 KB por nota, 50 resultados na busca |
| MitM em rede local tentando interceptar | A05 — host hardcoded como localhost only |
| Comprometimento sem detecção | A09 — audit log de TODA operação |
🔒 Controles implementados
A01 — Broken Access Control
// Bloqueia path traversal em todas as suas formas
function sanitizePath(p) {
if (!p || typeof p !== "string") throw new Error("path inválido");
const decoded = decodeURIComponent(p);
if (decoded.includes("..") || /[<>:"|?*]/.test(decoded)) {
throw new Error("caracteres não permitidos no caminho");
}
return decoded.replace(/\\/g, "/").replace(/^\/+/, "");
}Comportamento:
- Rejeita:
../outra-pasta,..\\system32,%2e%2e/, paths absolutos - Exige extensão
.mdemcreate_noteeedit_note - Path é sempre relativo à raiz do vault
A02 — Cryptographic Failures
- API key nunca hardcoded. Lida exclusivamente de
process.env.OBSIDIAN_API_KEY - Nunca logada em audit log. Função
sanitizeForLogfiltra qualquer string que pareça API key - Erro fatal no boot se variável não estiver definida
if (!CONFIG.apiKey) {
console.error("[FATAL] OBSIDIAN_API_KEY não definida no .env");
process.exit(1);
}A03 — Injection
Todos os inputs validados com Zod schemas antes de virar qualquer chamada HTTP:
server.tool(
"delete_note",
"Deleta uma nota do vault. ATENÇÃO: ação irreversível.",
{
path: z.string().describe("Caminho da nota a deletar"),
confirm: z.literal(true).describe("Deve ser true para confirmar a deleção"),
},
// ...
);- Zero uso de
eval,Function(),exec,spawn, ou qualquer execução dinâmica - Nenhuma chamada ao shell
A04 — Insecure Design
- Limite de tamanho: notas até 512 KB (
maxNoteSize) - Limite de resultados: busca retorna no máximo 50 matches
- Confirmação explícita pra operações destrutivas:
delete_noteexigeconfirm: z.literal(true)— Zod rejeita qualquer outro valoredit_notefaz backup do conteúdo anterior no audit log antes de sobrescrever
A05 — Security Misconfiguration
// Valida que a URL é apenas localhost
const parsed = new URL(url);
if (!["127.0.0.1", "localhost"].includes(parsed.hostname)) {
throw new Error("Host não permitido — apenas localhost (A05)");
}Mesmo que o usuário configure OBSIDIAN_HOST=http://maluco.com, o código rejeita em runtime.
A06 — Vulnerable Components
- Apenas 4 dependências runtime:
@modelcontextprotocol/sdk,zod,winston,dotenv npm auditna pipeline: zero vulnerabilidades reportadas- Sem deps pesadas (sem OpenCV, NumPy, Express, axios, lodash)
- Tarball de 6.4 KB empacotado
A09 — Logging & Monitoring
Todo call gera entrada estruturada em JSON no audit log com rotação:
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: path.join(CONFIG.logDir, "audit.log"),
maxsize: 5 * 1024 * 1024, // 5 MB por arquivo
maxFiles: 10, // mantém últimos 10
tailable: true,
}),
new winston.transports.File({
filename: path.join(CONFIG.logDir, "error.log"),
level: "error",
}),
],
});Cada linha:
{
"timestamp": "2026-04-26T18:23:45.123Z",
"action": "create_note",
"params": { "path": "Análises/2026-04/Foo.md", "size": 4382 },
"success": true,
"error": null,
"level": "info"
}Útil pra:
- Forense em caso de incidente
- Auditoria de compliance (LGPD / SOC 2)
- Detecção de comportamento anômalo (volume incomum de deletes etc.)
🧪 Validação prática
Os controles foram testados ao vivo durante o desenvolvimento. Exemplo de Zod recusando deleção sem confirm:
Tool: delete_note
Input: { "path": "test.md", "confirm": false }
Output:
MCP error -32602: Invalid arguments for tool delete_note: [
{
"received": false,
"code": "invalid_literal",
"expected": true,
"path": ["confirm"],
"message": "Invalid literal value, expected true"
}
]A operação destrutiva nem chega no plugin do Obsidian.
📊 Coverage do OWASP Top 10
| Top 10 (2021) | Aplicável a MCP server? | Coberto? |
|---|---|---|
| A01 — Broken Access Control | ✅ | ✅ |
| A02 — Cryptographic Failures | ✅ | ✅ |
| A03 — Injection | ✅ | ✅ |
| A04 — Insecure Design | ✅ | ✅ |
| A05 — Security Misconfiguration | ✅ | ✅ |
| A06 — Vulnerable & Outdated Components | ✅ | ✅ (dependências mínimas + npm audit) |
| A07 — Identification & Auth Failures | ⚠️ Parcial | API key via Bearer; sem suporte a multi-user (single-tenant por design) |
| A08 — Software & Data Integrity Failures | ⚠️ Parcial | npm publish com 2FA + GitHub releases assinados |
| A09 — Security Logging & Monitoring | ✅ | ✅ |
| A10 — Server-Side Request Forgery (SSRF) | ✅ | ✅ (host fixo localhost, sem URLs dinâmicas) |
Nota sobre A07/A08: servidor é single-tenant local por design — não há multi-user, e a integridade de distribuição depende da pipeline npm + GitHub. Tratamentos adicionais (assinatura de tarball, supply chain attestation) são roadmap.
🚨 Reportar vulnerabilidade
Encontrou problema de segurança? Por favor não abra issue público.
Reporte privadamente via:
- E-mail:
security@dewtech.tech - Ou GitHub Security Advisory: https://github.com/dewtech-technologies/obsidian-mcp-secure/security/advisories/new
Resposta esperada em até 48h úteis. Disclosure coordenado com 90 dias de janela padrão.
