Fix share token confusion vulnerability
Cache tokens from /api/send and share tokens were both signed with the same secret and parsed without distinguishing claim types, allowing a public cache token to be replayed as x-umami-share-token to read any website's analytics. Add explicit token type claims (share/cache) and require the matching type when parsing each.
This commit is contained in:
@@ -3,7 +3,7 @@ import { isbot } from 'isbot';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { z } from 'zod';
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { COLLECTION_TYPE, EVENT_TYPE } from '@/lib/constants';
|
||||
import { CACHE_TOKEN_TYPE, COLLECTION_TYPE, EVENT_TYPE } from '@/lib/constants';
|
||||
import { getSalt, hash, secret, uuid } from '@/lib/crypto';
|
||||
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
|
||||
import { createToken, parseToken } from '@/lib/jwt';
|
||||
@@ -106,7 +106,7 @@ export async function POST(request: Request) {
|
||||
if (cacheHeader) {
|
||||
const result = await parseToken(cacheHeader, secret());
|
||||
|
||||
if (result) {
|
||||
if (result?.type === CACHE_TOKEN_TYPE) {
|
||||
cache = result;
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,10 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
|
||||
const token = createToken(
|
||||
{ websiteId, sessionId, visitId, iat, type: CACHE_TOKEN_TYPE },
|
||||
secret(),
|
||||
);
|
||||
|
||||
return json({ cache: token, sessionId, visitId });
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBoardEntityIds } from '@/lib/boards';
|
||||
import { ENTITY_TYPE, ROLES } from '@/lib/constants';
|
||||
import { ENTITY_TYPE, ROLES, SHARE_TOKEN_TYPE } from '@/lib/constants';
|
||||
import { secret } from '@/lib/crypto';
|
||||
import { createToken } from '@/lib/jwt';
|
||||
import prisma from '@/lib/prisma';
|
||||
@@ -91,7 +91,7 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||
return notFound();
|
||||
}
|
||||
|
||||
data.token = createToken(data, secret());
|
||||
data.token = createToken({ ...data, type: SHARE_TOKEN_TYPE }, secret());
|
||||
|
||||
const accountId = await getAccountId(entity);
|
||||
|
||||
|
||||
+17
-2
@@ -1,5 +1,11 @@
|
||||
import debug from 'debug';
|
||||
import { ROLE_PERMISSIONS, ROLES, SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
||||
import {
|
||||
ROLE_PERMISSIONS,
|
||||
ROLES,
|
||||
SHARE_CONTEXT_HEADER,
|
||||
SHARE_TOKEN_HEADER,
|
||||
SHARE_TOKEN_TYPE,
|
||||
} from '@/lib/constants';
|
||||
import { createAuthKey, secret } from '@/lib/crypto';
|
||||
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
|
||||
import redis from '@/lib/redis';
|
||||
@@ -79,7 +85,16 @@ export async function hasPermission(role: string, permission: string | string[])
|
||||
|
||||
export function parseShareToken(request: Request) {
|
||||
try {
|
||||
return parseToken(request.headers.get(SHARE_TOKEN_HEADER), secret());
|
||||
const token: any = parseToken(request.headers.get(SHARE_TOKEN_HEADER), secret());
|
||||
|
||||
// Only accept tokens explicitly minted as share tokens. This prevents other
|
||||
// tokens signed with the same secret (e.g. the cache token from /api/send)
|
||||
// from being replayed as share tokens to gain analytics access.
|
||||
if (token?.type !== SHARE_TOKEN_TYPE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
|
||||
@@ -10,6 +10,8 @@ export const LAST_TEAM_CONFIG = 'umami.last-team';
|
||||
export const VERSION_CHECK = 'umami.version-check';
|
||||
export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
|
||||
export const SHARE_CONTEXT_HEADER = 'x-umami-share-context';
|
||||
export const SHARE_TOKEN_TYPE = 'share';
|
||||
export const CACHE_TOKEN_TYPE = 'cache';
|
||||
export const HOMEPAGE_URL = 'https://umami.is';
|
||||
export const DOCS_URL = 'https://umami.is/docs';
|
||||
export const REPO_URL = 'https://github.com/umami-software/umami';
|
||||
|
||||
Reference in New Issue
Block a user