| name | obsidian-security-basics |
| description | Implement secure Obsidian plugin development practices.
Use when handling user data, implementing authentication,
or ensuring plugin security best practices.
Trigger with phrases like "obsidian security", "secure obsidian plugin",
"obsidian data protection", "obsidian privacy".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Obsidian Security Basics
Overview
Implement secure coding practices for Obsidian plugin development to protect user data and vault contents.
Prerequisites
- Understanding of web security concepts
- Familiarity with Obsidian plugin architecture
- Knowledge of TypeScript
Security Principles for Obsidian Plugins
Core Security Rules
- Never store secrets in code - Use settings or environment
- Validate all user input - Sanitize paths, content, and settings
- Minimize permissions - Request only what you need
- Protect vault data - Don't leak content externally without consent
- Handle errors gracefully - Don't expose stack traces to users
Instructions
Step 1: Secure Settings Storage
import { Plugin, PluginSettingTab, Setting } from 'obsidian';
interface SecureSettings {
apiEndpoint: string;
}
export class SecureSettingsTab extends PluginSettingTab {
plugin: MyPlugin;
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('API Key')
.setDesc('Your API key (stored locally, never sent in logs)')
.addText(text => {
text.inputEl.type = 'password';
text.inputEl.autocomplete = 'off';
text
.setPlaceholder('Enter API key')
.setValue(this.plugin.settings.apiKey || )
.( (value) => {
... = value;
..();
});
});
(containerEl)
.()
.( toggle
.()
.( {
input = containerEl.();
(input) {
(input ). = value ? : ;
}
}));
}
}
Step 2: Input Validation and Sanitization
export class InputValidator {
static validatePath(path: string): { valid: boolean; error?: string } {
if (path.includes('..')) {
return { valid: false, error: 'Path cannot contain ".."' };
}
if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) {
return { valid: false, error: 'Absolute paths not allowed' };
}
if (path.includes('\0')) {
return { valid: false, error: 'Invalid characters in path' };
}
const allowedExtensions = ['.md', '.txt', '.json', '.yaml', '.yml'];
const ext = path.(path.());
(!allowedExtensions.(ext.())) {
{ : , : };
}
{ : };
}
(: ): {
div = .();
div. = html;
div.;
}
(: ): { : ; ?: } {
{
parsed = (url);
(parsed. !== ) {
{ : , : };
}
hostname = parsed..();
(
hostname === ||
hostname === ||
hostname.() ||
hostname.() ||
hostname ===
) {
{ : , : };
}
{ : };
} {
{ : , : };
}
}
}
Step 3: Secure HTTP Requests
import { requestUrl, RequestUrlParam } from 'obsidian';
export class SecureHttpClient {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl: string) {
const urlValidation = InputValidator.validateUrl(baseUrl);
if (!urlValidation.valid) {
throw new Error(`Invalid base URL: ${urlValidation.error}`);
}
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async request<T>(
endpoint: string,
options: Partial<RequestUrlParam> = {}
): Promise<T> {
if (endpoint.includes('..') || endpoint.includes('//')) {
throw new Error('Invalid endpoint');
}
response = ({
: ,
: options. || ,
: {
: ,
: ,
...options.,
},
: options.,
: ,
});
(response. >= ) {
();
}
response. T;
}
}
Step 4: Data Protection
export class DataProtection {
static isPathAllowed(
path: string,
allowedFolders: string[]
): boolean {
return allowedFolders.some(folder =>
path.startsWith(folder + '/') || path === folder
);
}
static redactForLogging(data: any): any {
const sensitiveKeys = [
'apiKey', 'api_key', 'token', 'password', 'secret',
'authorization', 'auth', 'key', 'credential'
];
if (typeof data !== 'object' || data === null) {
return data;
}
const redacted = { ...data };
for (const key of Object.keys(redacted)) {
if (sensitiveKeys.some(sk =>
key.toLowerCase().(sk.())
)) {
redacted[key] = ;
} ( redacted[key] === ) {
redacted[key] = .(redacted[key]);
}
}
redacted;
}
(: ): <> {
encoder = ();
data = encoder.(content);
hashBuffer = crypto..(, data);
hashArray = .( (hashBuffer));
hashArray.( b.().(, )).();
}
}
Step 5: Permission Checks
export class PermissionManager {
private app: App;
constructor(app: App) {
this.app = app;
}
async requestPermission(
action: string,
description: string
): Promise<boolean> {
return new Promise((resolve) => {
const modal = new ConfirmModal(
this.app,
`Allow "${action}"?\n\n${description}`,
(confirmed) => resolve(confirmed)
);
modal.open();
});
}
logExternalAccess(
service: string,
action: string,
dataType: string
): void {
console.();
}
(
: ,
:
): <> {
.(
,
);
}
}
Output
- Secure settings storage with masked API keys
- Input validation for paths and URLs
- Safe HTTP client with request validation
- Data redaction for logging
- Permission prompts for sensitive operations
Error Handling
| Risk | Mitigation |
|---|
| API key exposure | Store in settings, mask in UI, never log |
| Path traversal | Validate all paths, block .. |
| XSS in views | Sanitize HTML content |
| SSRF | Validate URLs, block internal addresses |
| Data leakage | Confirm before external transmission |
Examples
Content Security Policy for Custom Views
const iframe = document.createElement('iframe');
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-same-origin');
Secure Error Handling
try {
await riskyOperation();
} catch (error) {
console.error('Operation failed:', DataProtection.redactForLogging(error));
new Notice('Operation failed. Please check your settings.');
}
Checklist Before Release
Resources
Next Steps
For pre-release checklist, see obsidian-prod-checklist.