Skip to content

Commit

Permalink
address XSS #2
Browse files Browse the repository at this point in the history
  • Loading branch information
DanConwayDev committed Jun 7, 2023
1 parent a88ae26 commit 25c9303
Show file tree
Hide file tree
Showing 8 changed files with 3,306 additions and 9,481 deletions.
6,475 changes: 0 additions & 6,475 deletions package-lock.json

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 1 addition & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/LoadContactsPage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Event, nip05, nip19 } from 'nostr-tools';
import { sanitize } from 'isomorphic-dompurify';
import {
fetchAllCachedProfileEvents, fetchCachedMyProfileEvent, fetchCachedProfileEvent,
fetchProfileEvent, getContactMostPopularPetname, getContactName, getMyPetnameForUser,
Expand Down Expand Up @@ -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">
Expand Down
5 changes: 4 additions & 1 deletion src/LoadHistory.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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])}`));
Expand Down
11 changes: 6 additions & 5 deletions src/LoadMetadataPage.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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>
`;
Expand All @@ -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>
`;

Expand All @@ -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)}
Expand Down
18 changes: 12 additions & 6 deletions src/fetchEvents.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
};
Expand All @@ -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
Expand All @@ -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);
}
}
}
Expand Down
Loading

0 comments on commit 25c9303

Please sign in to comment.