security: don't send referrer paths for images and bookmarks (#3881)
We're currently sending `referrer` with path for image/bookmark requests. We shouldn't do that as it exposes the rooms to other servers. ## `<img>` - `<img>` tags have the right referrerpolicy to be `strict-origin-when-cross-origin`: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#referrerpolicy - _however_, because we use React, it looks like react creates a raw DOM node and adds properties one by one and it loses the default referrerpolicy it would otherwise get! So, in `BookmarkShapeUtil` we explicitly state the `referrerpolicy` - `background-image` does the right thing 👍 - _also_, I added this to places we do programmatic `new Image()` ## `fetch` - _however_, fetch does not! wtf. https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch it's almost a footnote in this section of the docs (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options) that `no-referrer-when-downgrade` is the default. ## `new Image()` ugh, but _also_ doing a programmatic `new Image()` doesn't do the right thing and we need to set the referrerpolicy here as well ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Test on staging that referrer with path isn't being sent anymore. ### Release Notes - Security: fix referrer being sent for bookmarks and images.
This commit is contained in:
parent
b04ded47c3
commit
b7bc2dbbce
16 changed files with 46 additions and 11 deletions
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
|
||||||
|
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||||
<meta name="theme-color" content="#FFFFFF" data-rh="true" />
|
<meta name="theme-color" content="#FFFFFF" data-rh="true" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
|
@ -17,6 +17,7 @@ function preloadIcon(url: string) {
|
||||||
;(image as any).fetchPriority = 'low'
|
;(image as any).fetchPriority = 'low'
|
||||||
image.onload = resolve
|
image.onload = resolve
|
||||||
image.onerror = reject
|
image.onerror = reject
|
||||||
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = url
|
image.src = url
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export async function createAssetFromFile({ file }: { type: 'file'; file: File }
|
||||||
await fetch(url, {
|
await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: file,
|
body: file,
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
})
|
})
|
||||||
|
|
||||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
||||||
|
|
|
@ -40,7 +40,11 @@ export async function createAssetFromUrl({ url }: { type: 'url'; url: string }):
|
||||||
let meta: { image: string; title: string; description: string }
|
let meta: { image: string; title: string; description: string }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
|
const resp = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'no-cors',
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
meta = {
|
meta = {
|
||||||
|
|
|
@ -30,7 +30,11 @@ export async function onCreateAssetFromUrl({
|
||||||
let meta: { image: string; title: string; description: string }
|
let meta: { image: string; title: string; description: string }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
|
const resp = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'no-cors',
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
meta = {
|
meta = {
|
||||||
|
|
|
@ -113,7 +113,11 @@ export function registerDefaultExternalContentHandlers(
|
||||||
let meta: { image: string; title: string; description: string }
|
let meta: { image: string; title: string; description: string }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
|
const resp = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'no-cors',
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
meta = {
|
meta = {
|
||||||
|
|
|
@ -61,6 +61,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
||||||
<img
|
<img
|
||||||
className="tl-bookmark__image"
|
className="tl-bookmark__image"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
src={asset?.props.image}
|
src={asset?.props.image}
|
||||||
alt={asset?.props.title || ''}
|
alt={asset?.props.title || ''}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
||||||
|
|
||||||
async function getDataURIFromURL(url: string): Promise<string> {
|
async function getDataURIFromURL(url: string): Promise<string> {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
return FileHelpers.blobToDataUrl(blob)
|
return FileHelpers.blobToDataUrl(blob)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
setStaticFrameSrc(canvas.toDataURL())
|
setStaticFrameSrc(canvas.toDataURL())
|
||||||
}
|
}
|
||||||
image.crossOrigin = 'anonymous'
|
image.crossOrigin = 'anonymous'
|
||||||
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = url
|
image.src = url
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -28,7 +28,9 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef
|
||||||
const fontFaceRule: string = (font as any).$$_fontface
|
const fontFaceRule: string = (font as any).$$_fontface
|
||||||
if (!url || !fontFaceRule) return null
|
if (!url || !fontFaceRule) return null
|
||||||
|
|
||||||
const fontFile = await (await fetch(url)).blob()
|
const fontFile = await (
|
||||||
|
await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
||||||
|
).blob()
|
||||||
const base64FontFile = await FileHelpers.blobToDataUrl(fontFile)
|
const base64FontFile = await FileHelpers.blobToDataUrl(fontFile)
|
||||||
|
|
||||||
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
||||||
|
|
|
@ -17,11 +17,13 @@ export function AssetUrlsProvider({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const src of Object.values(assetUrls.icons)) {
|
for (const src of Object.values(assetUrls.icons)) {
|
||||||
const image = new Image()
|
const image = new Image()
|
||||||
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = src
|
image.src = src
|
||||||
image.decode()
|
image.decode()
|
||||||
}
|
}
|
||||||
for (const src of Object.values(assetUrls.embedIcons)) {
|
for (const src of Object.values(assetUrls.embedIcons)) {
|
||||||
const image = new Image()
|
const image = new Image()
|
||||||
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = src
|
image.src = src
|
||||||
image.decode()
|
image.decode()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@ export async function pasteFiles(
|
||||||
point?: VecLike,
|
point?: VecLike,
|
||||||
sources?: TLExternalContentSource[]
|
sources?: TLExternalContentSource[]
|
||||||
) {
|
) {
|
||||||
const blobs = await Promise.all(urls.map(async (url) => await (await fetch(url)).blob()))
|
const blobs = await Promise.all(
|
||||||
|
urls.map(
|
||||||
|
async (url) =>
|
||||||
|
await (await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })).blob()
|
||||||
|
)
|
||||||
|
)
|
||||||
const files = blobs.map((blob) => new File([blob], 'tldrawFile', { type: blob.type }))
|
const files = blobs.map((blob) => new File([blob], 'tldrawFile', { type: blob.type }))
|
||||||
|
|
||||||
editor.mark('paste')
|
editor.mark('paste')
|
||||||
|
|
|
@ -20,7 +20,10 @@ export async function pasteUrl(
|
||||||
try {
|
try {
|
||||||
// skip this step if the url doesn't contain an image extension, treat it as a regular bookmark
|
// skip this step if the url doesn't contain an image extension, treat it as a regular bookmark
|
||||||
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
||||||
const resp = await fetch(url, { method: 'HEAD' })
|
const resp = await fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
})
|
||||||
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
||||||
editor.mark('paste')
|
editor.mark('paste')
|
||||||
pasteFiles(editor, [url])
|
pasteFiles(editor, [url])
|
||||||
|
|
|
@ -65,6 +65,7 @@ export async function getSvgAsImage(
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = svgUrl
|
image.src = svgUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,9 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// try to save the asset as a base64 string
|
// try to save the asset as a base64 string
|
||||||
assetSrcToSave = await FileHelpers.blobToDataUrl(
|
assetSrcToSave = await FileHelpers.blobToDataUrl(
|
||||||
await (await fetch(record.props.src)).blob()
|
await (
|
||||||
|
await fetch(record.props.src, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
||||||
|
).blob()
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// if that fails, just save the original src
|
// if that fails, just save the original src
|
||||||
|
|
|
@ -10,9 +10,11 @@ export class FileHelpers {
|
||||||
* from https://stackoverflow.com/a/53817185
|
* from https://stackoverflow.com/a/53817185
|
||||||
*/
|
*/
|
||||||
static async dataUrlToArrayBuffer(dataURL: string) {
|
static async dataUrlToArrayBuffer(dataURL: string) {
|
||||||
return fetch(dataURL).then(function (result) {
|
return fetch(dataURL, { referrerPolicy: 'strict-origin-when-cross-origin' }).then(
|
||||||
|
function (result) {
|
||||||
return result.arrayBuffer()
|
return result.arrayBuffer()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -72,6 +72,7 @@ export class MediaHelpers {
|
||||||
reject(new Error('Could not load image'))
|
reject(new Error('Could not load image'))
|
||||||
}
|
}
|
||||||
img.crossOrigin = 'anonymous'
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
img.src = src
|
img.src = src
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue