Skip to content
Snippets Groups Projects
Commit e254e461 authored by andree.klattenhoff's avatar andree.klattenhoff
Browse files

Fix: OXUIB-2698: SSRF/XSS: Prevent to cache resources from external origins

parent 02053924
No related branches found
No related tags found
No related merge requests found
......@@ -177,6 +177,8 @@ metadata:
data:
config.yaml: |
baseUrls:
- http://preview-app-core-ui
- http://preview-app-office-web
- http://preview-app-core-ui/manifest.json
- http://preview-app-office-web/manifest.json
- http://preview-app-guard-ui/manifest.json
- http://preview-app-core-guidedtours/manifest.json
---
......@@ -169,3 +169,15 @@ spec:
host: preview-sentinel-core-ui-middleware
port:
number: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: preview-core-ui-middleware-sentinel
data:
config.yaml: |
baseUrls:
- http://preview-app-core-ui/manifest.json
- http://preview-app-office-web/manifest.json
- http://preview-app-guard-ui/manifest.json
- http://preview-app-core-guidedtours/manifest.json
......@@ -31,7 +31,8 @@ core-ui-middleware:
mode: sentinel
hosts:
- preview-sentinel-redis:26379
existingConfigMap: preview-core-ui-middleware
existingConfigMap: preview-core-ui-middleware-sentinel
logLevel: info
defaultRegistry: ""
......
......@@ -173,8 +173,10 @@ metadata:
data:
config.yaml: |
baseUrls:
- http://preview-app-core-ui
- http://preview-app-office-web
- http://preview-app-core-ui/manifest.json
- http://preview-app-office-web/manifest.json
- http://preview-app-guard-ui/manifest.json
- http://preview-app-core-guidedtours/manifest.json
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
......
......@@ -5,6 +5,7 @@ global:
core-ui-middleware:
replicaCount: 1
existingConfigMap: preview-core-ui-middleware
logLevel: info
defaultRegistry: ""
......@@ -111,10 +112,10 @@ appsuite:
memory: "32Mi"
core-guidedtours:
enabled: false
enabled: true
guard-ui:
enabled: false
enabled: true
core-documents-collaboration:
enabled: false
......
......@@ -95,4 +95,9 @@ describe('File caching service', function () {
const response2 = await app.inject({ url: '/demo.js', headers: { version } })
expect(response2.statusCode).to.equal(200)
})
it('does not fetch from origins not defined in baseUrls', async function () {
const response = await app.inject({ url: '//t989be0.netlify.app/xss.html' })
expect(response.statusCode).to.equal(400)
})
})
......@@ -273,4 +273,9 @@ describe('File caching service', function () {
// check for files in redis
expect(await redis.client.getBuffer('ui-middleware:554855300:/file.svg:body')).to.deep.equal(response.rawPayload)
})
it('does not fetch from origins not defined in baseUrls', async function () {
const response = await app.inject({ url: '//t989be0.netlify.app/xss.html' })
expect(response.statusCode).to.equal(400)
})
})
......@@ -31,6 +31,7 @@ export const configMap = {
try {
const doc = yaml.load(await fs.readFile('./config/config.yaml', 'utf8'))
this.urls = doc.baseUrls || []
this.origins = doc.baseUrls.map((baseUrl) => (new URL(baseUrl)).origin)
this.salt = doc.salt
logger.debug('[Config] Config has been loaded')
} catch (error) {
......
......@@ -29,6 +29,13 @@ export class NotFoundError extends Error {
export class VersionMismatchError extends Error {}
export class NotAllowedOriginError extends Error {
constructor (message, options = {}) {
super(message, options)
this.status = options.status
}
}
/**
* Returns true, if the error is a VersionMismatchError or if the error is an aggregate error containing a VersionMismatchError
* @param {AggregateError | Error} err
......@@ -48,3 +55,8 @@ export function isNotFoundError (err) {
const errors = err instanceof AggregateError ? err.errors : [err]
return errors.some(error => error instanceof NotFoundError)
}
export function isNotAllowedOriginError (err) {
const errors = err instanceof AggregateError ? err.errors : [err]
return errors.some(error => error instanceof NotAllowedOriginError)
}
......@@ -25,7 +25,7 @@ import { promisify } from 'node:util'
import zlib from 'node:zlib'
import * as cache from './cache.js'
import { configMap } from './config_map.js'
import { NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js'
import { NotAllowedOriginError, NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js'
import logger from './logger.js'
import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
import { getVersionInfo } from './version.js'
......@@ -38,8 +38,13 @@ const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').replace(/([
const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})($|;)`, 'i')
export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) {
const upstreamUrl = new URL(path, baseUrl)
if (configMap.origins.includes(upstreamUrl.origin) === false) {
logger.debug(`"${upstreamUrl.origin}" does not match valid baseUrl: "${baseUrl}".`)
throw new NotAllowedOriginError('This origin is not allowed', { status: 400 })
}
const [response, dependencies] = await Promise.all([
fetch(new URL(path, baseUrl), { cache: 'no-store' }),
fetch(upstreamUrl, { cache: 'no-store' }),
nodePath.extname(path) === '.js' && getCSSDependenciesFor({ file: path.substr(1), version })
])
......
......@@ -21,7 +21,7 @@
*/
import { getFile } from '../files.js'
import { isNotFoundError, isVersionMismatchError } from '../errors.js'
import { isNotAllowedOriginError, isNotFoundError, isVersionMismatchError } from '../errors.js'
export default async function serveFilePlugin (fastify, options) {
fastify.get('*', async (req, reply) => {
......@@ -35,6 +35,7 @@ export default async function serveFilePlugin (fastify, options) {
reply.send(body)
} catch (err) {
if (isNotFoundError(err) || isVersionMismatchError(err)) throw fastify.httpErrors.createError(404, `File "${req.urlData('path')}" does not exist.`, err)
if (isNotAllowedOriginError(err)) throw fastify.httpErrors.createError(400, err)
throw fastify.httpErrors.createError(err.statusCode || 500, err)
}
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment