Merge pull request #8487 from RoadieHQ/search-location-protocol-whitelist

Drop Search Results with potentially unsafe location values
This commit is contained in:
Fredrik Adelöw
2021-12-16 21:19:15 +01:00
committed by GitHub
3 changed files with 89 additions and 2 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-search-backend': minor
---
Search result location filtering
This change introduces a filter for search results based on their location protocol. The intention is to filter out unsafe or
malicious values before they can be consumed by the frontend. By default locations must be http/https URLs (or paths).
@@ -18,6 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common';
import {
IndexBuilder,
LunrSearchEngine,
SearchEngine,
} from '@backstage/plugin-search-backend-node';
import express from 'express';
import request from 'supertest';
@@ -26,11 +27,13 @@ import { createRouter } from './router';
describe('createRouter', () => {
let app: express.Express;
let mockSearchEngine: jest.Mocked<SearchEngine>;
beforeAll(async () => {
const logger = getVoidLogger();
const searchEngine = new LunrSearchEngine({ logger });
const indexBuilder = new IndexBuilder({ logger, searchEngine });
const router = await createRouter({
engine: indexBuilder.getSearchEngine(),
logger,
@@ -49,5 +52,63 @@ describe('createRouter', () => {
expect(response.status).toEqual(200);
expect(response.body).toMatchObject({ results: [] });
});
describe('search result filtering', () => {
beforeAll(async () => {
const logger = getVoidLogger();
mockSearchEngine = {
index: jest.fn(),
setTranslator: jest.fn(),
query: jest.fn(),
};
const indexBuilder = new IndexBuilder({
logger,
searchEngine: mockSearchEngine,
});
const router = await createRouter({
engine: indexBuilder.getSearchEngine(),
logger,
});
app = express().use(router);
});
describe('where the search result set includes unsafe results', () => {
const safeResult = {
type: 'software-catalog',
document: {
text: 'safe',
title: 'safe-location',
// eslint-disable-next-line no-script-url
location: '/catalog/default/component/safe',
},
};
beforeEach(() => {
mockSearchEngine.query.mockResolvedValue({
results: [
{
type: 'software-catalog',
document: {
text: 'unsafe',
title: 'unsafe-location',
// eslint-disable-next-line no-script-url
location: 'javascript:alert("unsafe")',
},
},
safeResult,
],
nextPageCursor: '',
previousPageCursor: '',
});
});
it('removes the unsafe results', async () => {
const response = await request(app).get('/query');
expect(response.status).toEqual(200);
expect(response.body).toMatchObject({ results: [safeResult] });
});
});
});
});
});
+20 -2
View File
@@ -25,10 +25,28 @@ export type RouterOptions = {
logger: Logger;
};
const allowedLocationProtocols = ['http:', 'https:'];
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { engine, logger } = options;
const filterResultSet = ({ results, ...resultSet }: SearchResultSet) => ({
...resultSet,
results: results.filter(result => {
const protocol = new URL(result.document.location, 'https://example.com')
.protocol;
const isAllowed = allowedLocationProtocols.includes(protocol);
if (!isAllowed) {
logger.info(
`Rejected search result for "${result.document.title}" as location protocol "${protocol}" is unsafe`,
);
}
return isAllowed;
}),
});
const router = Router();
router.get(
'/query',
@@ -46,8 +64,8 @@ export async function createRouter(
);
try {
const results = await engine?.query(req.query);
res.send(results);
const resultSet = await engine?.query(req.query);
res.send(filterResultSet(resultSet));
} catch (err) {
throw new Error(
`There was a problem performing the search query. ${err}`,