fix for media path traversal
This commit is contained in:
parent
42ad089b04
commit
e3afede989
|
|
@ -193,6 +193,7 @@ function noteApp() {
|
||||||
// Preview rendering debounce
|
// Preview rendering debounce
|
||||||
_previewDebounceTimeout: null,
|
_previewDebounceTimeout: null,
|
||||||
_lastRenderedContent: '',
|
_lastRenderedContent: '',
|
||||||
|
_lastRenderedNote: '',
|
||||||
_cachedRenderedHTML: '',
|
_cachedRenderedHTML: '',
|
||||||
_mathDebounceTimeout: null,
|
_mathDebounceTimeout: null,
|
||||||
_mermaidDebounceTimeout: null,
|
_mermaidDebounceTimeout: null,
|
||||||
|
|
@ -1287,6 +1288,49 @@ function noteApp() {
|
||||||
return this._mediaLookup.get(nameLower) || null;
|
return this._mediaLookup.get(nameLower) || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Resolve a Markdown image path to a vault-relative path (forward slashes).
|
||||||
|
// Relative paths use the note file's directory as base (same rules as CommonMark / browsers).
|
||||||
|
// A leading "/" (not "//") means vault-root-relative.
|
||||||
|
resolveMarkdownMediaPathForNote(noteVaultPath, rawSrc) {
|
||||||
|
const pathPart = rawSrc.split('#')[0].split('?')[0];
|
||||||
|
if (!pathPart) return '';
|
||||||
|
if (pathPart.startsWith('/') && !pathPart.startsWith('//')) {
|
||||||
|
return pathPart.replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
if (!noteVaultPath) {
|
||||||
|
return pathPart;
|
||||||
|
}
|
||||||
|
const dir = noteVaultPath.includes('/')
|
||||||
|
? noteVaultPath.slice(0, noteVaultPath.lastIndexOf('/'))
|
||||||
|
: '';
|
||||||
|
const base = `https://vn.invalid/${dir ? `${dir}/` : ''}`;
|
||||||
|
try {
|
||||||
|
const u = new URL(pathPart, base);
|
||||||
|
let p = u.pathname;
|
||||||
|
if (p.startsWith('/')) p = p.slice(1);
|
||||||
|
return p.split('/').filter(seg => seg !== '').map(seg => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(seg);
|
||||||
|
} catch (e) {
|
||||||
|
return seg;
|
||||||
|
}
|
||||||
|
}).join('/');
|
||||||
|
} catch (e) {
|
||||||
|
return pathPart;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
encodeVaultRelativePathForMediaApi(vaultRelativePath) {
|
||||||
|
if (!vaultRelativePath) return '';
|
||||||
|
return vaultRelativePath.split('/').map(segment => {
|
||||||
|
try {
|
||||||
|
return encodeURIComponent(decodeURIComponent(segment));
|
||||||
|
} catch (e) {
|
||||||
|
return encodeURIComponent(segment);
|
||||||
|
}
|
||||||
|
}).join('/');
|
||||||
|
},
|
||||||
|
|
||||||
// Load all tags
|
// Load all tags
|
||||||
async loadTags() {
|
async loadTags() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2858,6 +2902,7 @@ function noteApp() {
|
||||||
|
|
||||||
this.currentNote = notePath;
|
this.currentNote = notePath;
|
||||||
this._lastRenderedContent = ''; // Clear render cache for new note
|
this._lastRenderedContent = ''; // Clear render cache for new note
|
||||||
|
this._lastRenderedNote = '';
|
||||||
this._cachedRenderedHTML = '';
|
this._cachedRenderedHTML = '';
|
||||||
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
||||||
this.noteContent = data.content;
|
this.noteContent = data.content;
|
||||||
|
|
@ -3962,6 +4007,7 @@ function noteApp() {
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
this.currentNoteName = '';
|
this.currentNoteName = '';
|
||||||
this._lastRenderedContent = ''; // Clear render cache
|
this._lastRenderedContent = ''; // Clear render cache
|
||||||
|
this._lastRenderedNote = '';
|
||||||
this._cachedRenderedHTML = '';
|
this._cachedRenderedHTML = '';
|
||||||
document.title = this.appName;
|
document.title = this.appName;
|
||||||
// Redirect to root
|
// Redirect to root
|
||||||
|
|
@ -4133,7 +4179,9 @@ function noteApp() {
|
||||||
if (!this.noteContent) return '<p style="color: var(--text-tertiary);">Nothing to preview yet...</p>';
|
if (!this.noteContent) return '<p style="color: var(--text-tertiary);">Nothing to preview yet...</p>';
|
||||||
|
|
||||||
// Performance: Return cached HTML if content hasn't changed
|
// Performance: Return cached HTML if content hasn't changed
|
||||||
if (this.noteContent === this._lastRenderedContent && this._cachedRenderedHTML) {
|
if (this.noteContent === this._lastRenderedContent &&
|
||||||
|
this.currentNote === this._lastRenderedNote &&
|
||||||
|
this._cachedRenderedHTML) {
|
||||||
return this._cachedRenderedHTML;
|
return this._cachedRenderedHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4327,14 +4375,10 @@ function noteApp() {
|
||||||
|
|
||||||
// Transform relative paths to /api/media/ for serving
|
// Transform relative paths to /api/media/ for serving
|
||||||
if (isLocal && !src.startsWith('/api/media/')) {
|
if (isLocal && !src.startsWith('/api/media/')) {
|
||||||
// URL-encode path segments to handle spaces and special characters
|
const vaultRelative = self.currentNote
|
||||||
const encodedPath = src.split('/').map(segment => {
|
? self.resolveMarkdownMediaPathForNote(self.currentNote, src)
|
||||||
try {
|
: src.split('#')[0].split('?')[0];
|
||||||
return encodeURIComponent(decodeURIComponent(segment));
|
const encodedPath = self.encodeVaultRelativePathForMediaApi(vaultRelative);
|
||||||
} catch (e) {
|
|
||||||
return encodeURIComponent(segment);
|
|
||||||
}
|
|
||||||
}).join('/');
|
|
||||||
src = `/api/media/${encodedPath}`;
|
src = `/api/media/${encodedPath}`;
|
||||||
img.setAttribute('src', src);
|
img.setAttribute('src', src);
|
||||||
}
|
}
|
||||||
|
|
@ -4434,6 +4478,7 @@ function noteApp() {
|
||||||
|
|
||||||
// Cache the result for performance
|
// Cache the result for performance
|
||||||
this._lastRenderedContent = this.noteContent;
|
this._lastRenderedContent = this.noteContent;
|
||||||
|
this._lastRenderedNote = this.currentNote;
|
||||||
this._cachedRenderedHTML = html;
|
this._cachedRenderedHTML = html;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue