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" />
|
||||
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||
<meta name="theme-color" content="#FFFFFF" data-rh="true" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
|
|
@ -17,6 +17,7 @@ function preloadIcon(url: string) {
|
|||
;(image as any).fetchPriority = 'low'
|
||||
image.onload = resolve
|
||||
image.onerror = reject
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = url
|
||||
})
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export async function createAssetFromFile({ file }: { type: 'file'; file: File }
|
|||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
|
||||
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 }
|
||||
|
||||
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 doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
meta = {
|
||||
|
|
|
@ -30,7 +30,11 @@ export async function onCreateAssetFromUrl({
|
|||
let meta: { image: string; title: string; description: string }
|
||||
|
||||
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 doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
meta = {
|
||||
|
|
|
@ -113,7 +113,11 @@ export function registerDefaultExternalContentHandlers(
|
|||
let meta: { image: string; title: string; description: string }
|
||||
|
||||
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 doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
meta = {
|
||||
|
|
|
@ -61,6 +61,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|||
<img
|
||||
className="tl-bookmark__image"
|
||||
draggable={false}
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
src={asset?.props.image}
|
||||
alt={asset?.props.title || ''}
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|||
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
||||
|
||||
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()
|
||||
return FileHelpers.blobToDataUrl(blob)
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
setStaticFrameSrc(canvas.toDataURL())
|
||||
}
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = url
|
||||
|
||||
return () => {
|
||||
|
|
|
@ -28,7 +28,9 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef
|
|||
const fontFaceRule: string = (font as any).$$_fontface
|
||||
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 newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
||||
|
|
|
@ -17,11 +17,13 @@ export function AssetUrlsProvider({
|
|||
useEffect(() => {
|
||||
for (const src of Object.values(assetUrls.icons)) {
|
||||
const image = new Image()
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = src
|
||||
image.decode()
|
||||
}
|
||||
for (const src of Object.values(assetUrls.embedIcons)) {
|
||||
const image = new Image()
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = src
|
||||
image.decode()
|
||||
}
|
||||
|
|
|
@ -14,7 +14,12 @@ export async function pasteFiles(
|
|||
point?: VecLike,
|
||||
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 }))
|
||||
|
||||
editor.mark('paste')
|
||||
|
|
|
@ -20,7 +20,10 @@ export async function pasteUrl(
|
|||
try {
|
||||
// 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)) {
|
||||
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\//)) {
|
||||
editor.mark('paste')
|
||||
pasteFiles(editor, [url])
|
||||
|
|
|
@ -65,6 +65,7 @@ export async function getSvgAsImage(
|
|||
resolve(null)
|
||||
}
|
||||
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = svgUrl
|
||||
})
|
||||
|
||||
|
|
|
@ -188,7 +188,9 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
|||
try {
|
||||
// try to save the asset as a base64 string
|
||||
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 {
|
||||
// if that fails, just save the original src
|
||||
|
|
|
@ -10,9 +10,11 @@ export class FileHelpers {
|
|||
* from https://stackoverflow.com/a/53817185
|
||||
*/
|
||||
static async dataUrlToArrayBuffer(dataURL: string) {
|
||||
return fetch(dataURL).then(function (result) {
|
||||
return result.arrayBuffer()
|
||||
})
|
||||
return fetch(dataURL, { referrerPolicy: 'strict-origin-when-cross-origin' }).then(
|
||||
function (result) {
|
||||
return result.arrayBuffer()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -72,6 +72,7 @@ export class MediaHelpers {
|
|||
reject(new Error('Could not load image'))
|
||||
}
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue