mirror of
https://github.com/aljazceru/nostr-profile-manager.git
synced 2025-12-17 05:34:20 +01:00
address XSS #2
This commit is contained in:
6475
package-lock.json
generated
6475
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^1.5.7",
|
||||
"isomorphic-dompurify": "^1.6.0",
|
||||
"nostr-tools": "^1.5.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"websocket-polyfill": "^0.0.3"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Lightweight typescript micro app for basic nostr profile management. Current USP is offline backup and restore.
|
||||
|
||||
Only javascript dependency is [nostr-tools](https://github.com/nbd-wtf/nostr-tools). no JS frameworks. no state management tools.
|
||||
Minimial javascript dependencies. no JS frameworks. no state management tools.
|
||||
|
||||
## Live instances
|
||||
|
||||
@@ -55,7 +55,6 @@ Supported profile events: kind `0`, `10002` and `3`.
|
||||
- [ ] look far and wide for events
|
||||
- cycle through all known relays to find current and previous versions of profile events to enable restoration. reccommended only when accessed through a VPN
|
||||
##### Lightweight
|
||||
- [ ] only javascript dependancy is nostr-tools (TODO: remove timeago)
|
||||
- [x] connects to the minimum number of relays
|
||||
- [x] connect relays specified in `10002` or 3 default relays
|
||||
- [ ] minimises the number of open websockets
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Event, nip05, nip19 } from 'nostr-tools';
|
||||
import { sanitize } from 'isomorphic-dompurify';
|
||||
import {
|
||||
fetchAllCachedProfileEvents, fetchCachedMyProfileEvent, fetchCachedProfileEvent,
|
||||
fetchProfileEvent, getContactMostPopularPetname, getContactName, getMyPetnameForUser,
|
||||
@@ -53,12 +54,12 @@ const generateContactDetails = (pubkey:string):string => {
|
||||
return `
|
||||
<article>
|
||||
<div>
|
||||
${m && !!m.picture ? `<img src="${m.picture}" /> ` : ''}
|
||||
${m && !!m.picture ? `<img src="${sanitize(m.picture)}" /> ` : ''}
|
||||
<div class="contactdetailsmain">
|
||||
<strong>${getContactName(pubkey)}</strong>
|
||||
${m.nip05 ? `<small id="nip05-${pubkey}">${m.nip05} </small>` : ''}<span id="nip05-${pubkey}-verified"></span>
|
||||
${m.nip05 ? `<small id="nip05-${pubkey}">${sanitize(m.nip05)} </small>` : ''}<span id="nip05-${pubkey}-verified"></span>
|
||||
${otherspetname && otherspetname !== m.name ? `<div>popular petname: ${otherspetname}</div>` : ''}
|
||||
<div><small>${m.about ? m.about : ''}</small></div>
|
||||
<div><small>${m.about ? sanitize(m.about) : ''}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="contactdetailsform">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as timeago from 'timeago.js';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { sanitize } from 'isomorphic-dompurify';
|
||||
import { fetchCachedMyProfileEventHistory, getContactName, submitUnsignedEvent } from './fetchEvents';
|
||||
|
||||
export type VersionChange = {
|
||||
@@ -26,7 +27,9 @@ export const generateMetadataChanges = (
|
||||
):VersionChange[] => history.map((e, i, a) => {
|
||||
const changes:string[] = [];
|
||||
const c = JSON.parse(e.content);
|
||||
const clean = (s:string | number) => (typeof s === 'string' ? s.replace(/(\r\n|\n|\r)/gm, ' ') : s.toString());
|
||||
const clean = (s:string | number) => (sanitize(
|
||||
typeof s === 'string' ? s.replace(/(\r\n|\n|\r)/gm, ' ') : s.toString(),
|
||||
));
|
||||
// if first backup list all fields and values
|
||||
if (i === a.length - 1) {
|
||||
Object.keys(c).forEach((k) => changes.push(`${k}: ${clean(c[k])}`));
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { nip05 } from 'nostr-tools';
|
||||
import { sanitize } from 'isomorphic-dompurify';
|
||||
import { fetchCachedMyProfileEvent, submitUnsignedEvent } from './fetchEvents';
|
||||
import { loadBackupHistory } from './LoadHistory';
|
||||
import { localStorageGetItem } from './LocalStorage';
|
||||
|
||||
type MetadataCore = {
|
||||
name: string;
|
||||
profile?: string;
|
||||
picture?: string;
|
||||
about?: string;
|
||||
banner?: string;
|
||||
nip05?: string;
|
||||
@@ -24,7 +25,7 @@ const toTextInput = (prop:string, m:MetadataFlex | null, displayname?:string) =>
|
||||
type="text"
|
||||
name="PM-form-${prop}"
|
||||
id="PM-form-${prop}"
|
||||
placeholder="${displayname || prop}" ${m && m[prop] ? `value="${m[prop]}"` : ''}
|
||||
placeholder="${displayname || prop}" ${m && m[prop] ? `value="${sanitize(m[prop] as string)}"` : ''}
|
||||
/>
|
||||
</label>
|
||||
`;
|
||||
@@ -35,7 +36,7 @@ const toTextarea = (prop:string, m:MetadataFlex | null, displayname?:string) =>
|
||||
id="PM-form-${prop}"
|
||||
name="PM-form-${prop}"
|
||||
placeholder="${displayname || prop}"
|
||||
>${m && m[prop] ? m[prop] : ''}</textarea>
|
||||
>${m && m[prop] ? sanitize(m[prop] as string) : ''}</textarea>
|
||||
</label>
|
||||
`;
|
||||
|
||||
@@ -57,9 +58,9 @@ const generateForm = (c:MetadataFlex | null):string => {
|
||||
${toTextInput('nip05', c)}
|
||||
</div>
|
||||
${toTextarea('about', c)}
|
||||
<img id="metadata-form-picture" src="${c && c.picture ? c.picture : ''}">
|
||||
<img id="metadata-form-picture" src="${c && c.picture ? sanitize(c.picture) : ''}">
|
||||
${toTextInput('picture', c)}
|
||||
<img id="metadata-form-banner" src="${c && c.banner ? c.banner : ''}">
|
||||
<img id="metadata-form-banner" src="${c && c.banner ? sanitize(c.banner) : ''}">
|
||||
${toTextInput('banner', c)}
|
||||
${toTextInput('lud06', c, 'lud06 (LNURL)')}
|
||||
${toTextInput('lud16', c)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Event, UnsignedEvent } from 'nostr-tools';
|
||||
import { sanitize } from 'isomorphic-dompurify';
|
||||
import { localStorageGetItem, localStorageSetItem } from './LocalStorage';
|
||||
import { publishEventToRelay, requestEventsFromRelays } from './RelayManagement';
|
||||
|
||||
@@ -210,6 +211,7 @@ export const fetchProfileEvent = async (
|
||||
return r[0];
|
||||
};
|
||||
|
||||
/** returns sanatized most popular petname for contact */
|
||||
export const getContactMostPopularPetname = (pubkey: string):string | null => {
|
||||
// considered implementing frank.david.erin model in nip-02 but I think the UX is to confusing
|
||||
// get count of petnames for users by other contacts
|
||||
@@ -218,7 +220,7 @@ export const getContactMostPopularPetname = (pubkey: string):string | null => {
|
||||
.map((pk) => {
|
||||
if (!UserProfileEvents[pk][3]) return null;
|
||||
const petnametag = UserProfileEvents[pk][3].tags.find((t) => t[1] === pubkey && t[3]);
|
||||
if (petnametag) return petnametag[3];
|
||||
if (petnametag) return sanitize(petnametag[3]);
|
||||
return null;
|
||||
})
|
||||
// returns petname counts
|
||||
@@ -229,14 +231,17 @@ export const getContactMostPopularPetname = (pubkey: string):string | null => {
|
||||
}, {} as { [petname: string]: number });
|
||||
if (petnamecounts.length === 0) return null;
|
||||
// returns most frequent petname for user amoung contacts (appended with ' (?)')
|
||||
return Object.keys(petnamecounts).sort((a, b) => petnamecounts[b] - petnamecounts[a])[0];
|
||||
return sanitize(
|
||||
Object.keys(petnamecounts).sort((a, b) => petnamecounts[b] - petnamecounts[a])[0],
|
||||
);
|
||||
};
|
||||
|
||||
/** returns my petname for user but sanatized */
|
||||
export const getMyPetnameForUser = (pubkey: string): string | null => {
|
||||
const e = fetchCachedMyProfileEvent(3);
|
||||
if (e) {
|
||||
const mypetname = e.tags.find((t) => t[1] === pubkey && t[3]);
|
||||
if (mypetname) return mypetname[3];
|
||||
if (mypetname) return sanitize(mypetname[3]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -259,13 +264,14 @@ export const isUserMyContact = (pubkey: string): boolean | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/** get sanatized contact name */
|
||||
export const getContactName = (pubkey: string):string => {
|
||||
// my own name
|
||||
if (localStorageGetItem('pubkey') === pubkey) {
|
||||
const m = fetchCachedMyProfileEvent(0);
|
||||
if (m) {
|
||||
const { name } = JSON.parse(m.content);
|
||||
if (name) return name;
|
||||
if (name) return sanitize(name);
|
||||
}
|
||||
} else {
|
||||
// my petname for contact
|
||||
@@ -277,9 +283,9 @@ export const getContactName = (pubkey: string):string => {
|
||||
if (UserProfileEvents[pubkey][0]) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { name, display_name } = JSON.parse(UserProfileEvents[pubkey][0].content);
|
||||
if (name) return name;
|
||||
if (name) return sanitize(name);
|
||||
// name isn't present for Jack Dorsey and Vitor from Amethyst in Apr 2023.
|
||||
if (display_name) return display_name;
|
||||
if (display_name) return sanitize(display_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user