feat: allow wildcard web origins for local tunnel testing
This commit is contained in:
1 parent
2a650853ee
commit
e4f4ff42e1
9 files changed
+115
-70
No files matched your search
@@ -1,4 +1,5 @@
|
||||
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
|
||||
PORT=3100
|
||||
WEB_ORIGIN=http://localhost:5173
|
||||
COOKIE_SECURE=false
|
||||
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
|
||||
PORT=3100
|
||||
# Local tunnel testing may use WEB_ORIGIN=*. Production requires an exact origin.
|
||||
WEB_ORIGIN=http://localhost:5173
|
||||
COOKIE_SECURE=false
|
||||
+19
-1
@@ -22,6 +22,20 @@ import { credentials } from './validation';
|
||||
export type UserRequest = Request & { userId: string; sessionId: string; revealed: boolean };
|
||||
const Public = () => SetMetadata('public', true);
|
||||
const digest = (s: string) => createHash('sha256').update(s).digest('hex');
|
||||
export function allowedOrigin(
|
||||
origin: string | undefined,
|
||||
configured: string | undefined,
|
||||
production = false,
|
||||
) {
|
||||
if (configured !== '*') return !!origin && origin === configured;
|
||||
if (production || !origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return ['http:', 'https:'].includes(url.protocol) && url.origin === origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private attempts = new Map<string, { count: number; until: number }>();
|
||||
@@ -87,7 +101,11 @@ export class AuthGuard implements CanActivate {
|
||||
const req = ctx.switchToHttp().getRequest<UserRequest>();
|
||||
if (
|
||||
!['GET', 'HEAD', 'OPTIONS'].includes(req.method) &&
|
||||
req.headers.origin !== process.env.WEB_ORIGIN
|
||||
!allowedOrigin(
|
||||
req.headers.origin,
|
||||
process.env.WEB_ORIGIN,
|
||||
process.env.NODE_ENV === 'production',
|
||||
)
|
||||
)
|
||||
throw new ForbiddenException('请求来源不受信任');
|
||||
if (this.reflector.get<boolean>('public', ctx.getHandler())) return true;
|
||||
|
||||
@@ -48,6 +48,8 @@ class AppModule {}
|
||||
async function bootstrap() {
|
||||
if (!process.env.DATABASE_URL || !process.env.WEB_ORIGIN)
|
||||
throw Error('Missing local environment configuration');
|
||||
if (process.env.NODE_ENV === 'production' && process.env.WEB_ORIGIN === '*')
|
||||
throw Error('Production requires an explicit web origin');
|
||||
if (process.env.NODE_ENV === 'production' && process.env.COOKIE_SECURE !== 'true')
|
||||
throw Error('Production requires secure cookies');
|
||||
const app = await NestFactory.create(AppModule, { logger: false, bodyParser: false });
|
||||
|
||||
@@ -6,6 +6,22 @@ import { positionInput, date, amount, businessDate, toBusinessDate } from '../sr
|
||||
import { validateBackup } from '../src/backup';
|
||||
import { RatesService } from '../src/rates';
|
||||
import { Database } from '../src/database';
|
||||
import { allowedOrigin } from '../src/auth';
|
||||
test('development wildcard accepts HTTP origins while exact and production checks stay strict', () => {
|
||||
assert.equal(allowedOrigin('https://tunnel.example', '*'), true);
|
||||
assert.equal(allowedOrigin('http://example.test:8080', '*'), true);
|
||||
for (const origin of [
|
||||
undefined,
|
||||
'null',
|
||||
'file://host',
|
||||
'https://example.test/path',
|
||||
'https://name:secret@example.test',
|
||||
])
|
||||
assert.equal(allowedOrigin(origin, '*'), false);
|
||||
assert.equal(allowedOrigin('https://tunnel.example', '*', true), false);
|
||||
assert.equal(allowedOrigin('https://tunnel.example', 'http://localhost:5173'), false);
|
||||
assert.equal(allowedOrigin('http://localhost:5173', 'http://localhost:5173'), true);
|
||||
});
|
||||
const rev = (amount: string, day: string) => ({
|
||||
id: randomUUID(),
|
||||
amount,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import { today } from '../src/validation';
|
||||
import { readBackupZip } from '../src/zip';
|
||||
const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api',
|
||||
origin = process.env.WEB_ORIGIN!;
|
||||
origin = process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
|
||||
test('real MySQL: authentication, isolation, history, backup and atomic failures', async () => {
|
||||
const db = new PrismaClient(),
|
||||
created: { id: string; username: string }[] = [];
|
||||
@@ -53,10 +53,10 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://untrusted.invalid',
|
||||
},
|
||||
body: JSON.stringify({ baseCurrency: 'USD' }),
|
||||
body: JSON.stringify({ showSidebar: true }),
|
||||
})
|
||||
).status,
|
||||
403,
|
||||
process.env.WEB_ORIGIN === '*' ? 200 : 403,
|
||||
);
|
||||
const make = async (
|
||||
kind: string,
|
||||
|
||||
@@ -12,7 +12,7 @@ test('privacy, minute history, backup-gated clear and idle sessions remain user
|
||||
const res = await fetch(base + path, {
|
||||
method,
|
||||
headers: {
|
||||
Origin: process.env.WEB_ORIGIN!,
|
||||
Origin: process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!,
|
||||
Cookie: cookie,
|
||||
...(data ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
@@ -131,7 +131,11 @@ test('privacy, minute history, backup-gated clear and idle sessions remain user
|
||||
form.append('file', new Blob([content]), 'backup.zip');
|
||||
const res = await fetch(base + '/backup/upload', {
|
||||
method: 'POST',
|
||||
headers: { Origin: process.env.WEB_ORIGIN!, Cookie: cookie },
|
||||
headers: {
|
||||
Origin:
|
||||
process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!,
|
||||
Cookie: cookie,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
return { status: res.status, data: await res.json() };
|
||||
|
||||
Reference in new issue
Block a user