diff --git a/src/server/request-handler.ts b/src/server/request-handler.ts
index 3f96c56..394e266 100644
--- a/src/server/request-handler.ts
+++ b/src/server/request-handler.ts
@@ -19,8 +19,22 @@ type ShareListingEntry = {
isCollection: boolean;
};
+function parseHttpUrl(value: string, errorMessage: string): URL {
+ const url = new URL(value.trim());
+
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
+ throw new Error(errorMessage);
+ }
+
+ if (url.username || url.password) {
+ throw new Error("URL credentials are not allowed.");
+ }
+
+ return url;
+}
+
function resolveDavUrlFromShareUrl(shareUrl: string): string {
- const url = new URL(shareUrl);
+ const url = parseHttpUrl(shareUrl, "Please enter a public Nextcloud share link.");
const publicShare = url.pathname.match(/\/s\/([^/?#]+)/);
if (publicShare) {
@@ -36,6 +50,17 @@ function resolveDavUrlFromShareUrl(shareUrl: string): string {
throw new Error("Please enter a public Nextcloud share link.");
}
+function resolveValidatedBlobUrl(targetUrl: string, shareUrl: string): string {
+ const target = parseHttpUrl(targetUrl, "Invalid image URL.");
+ const shareDavUrl = new URL(resolveDavUrlFromShareUrl(shareUrl));
+
+ if (target.origin !== shareDavUrl.origin || !target.pathname.startsWith(shareDavUrl.pathname)) {
+ throw new Error("Image URL is outside the requested Nextcloud share.");
+ }
+
+ return target.toString();
+}
+
async function proxyUpstream(url: string, init?: RequestInit) {
const upstream = await fetch(url, init);
const body = await upstream.arrayBuffer();
@@ -1018,6 +1043,16 @@ function htmlPage(): string {
return element;
}
+ function escapeHtml(value) {
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'"
+ })[character]);
+ }
+
function setTheme(theme) {
const resolvedTheme = theme === "light" ? "light" : "dark";
document.body.dataset.theme = resolvedTheme;
@@ -1383,7 +1418,7 @@ function htmlPage(): string {
return L.divIcon({
className: "photo-map-marker" + activeClass,
html:
- '',
+ '
',
iconSize: [44, 44],
iconAnchor: [22, 22],
popupAnchor: [0, -22]
@@ -1588,10 +1623,10 @@ function htmlPage(): string {
marker.bindPopup(
'