fix(nextcloud-files): WR-09/IN-02 ungueltige Pfadzeichen sind 400, Vorschau ohne SVG
- WR-09: ein einzelnes UTF-16-Ersatzzeichen in einem Pfadsegment ist 400 invalidPath (bzw. invalidName) statt eines URIError mit 500; die Weboberflaeche wiederholt keine 4xx - IN-02: Vorschaubilder nur als Rasterbild, image/svg+xml wird wie ein fehlendes Vorschaubild behandelt (CSP-Sandbox und nosniff bleiben) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,19 @@ describe('NextcloudFilesService — preview', () => {
|
|||||||
expect(res.headers).toEqual({});
|
expect(res.headers).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('image/svg+xml als Vorschau -> 404, nichts geschrieben (IN-02)', async () => {
|
||||||
|
const { service } = setup({
|
||||||
|
status: 200,
|
||||||
|
text: '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>',
|
||||||
|
headers: { 'content-type': 'image/svg+xml' },
|
||||||
|
});
|
||||||
|
const res = new FakeRes();
|
||||||
|
await expect(service.preview(res as never, 't1', 'u1', '42')).rejects.toMatchObject({
|
||||||
|
response: { code: 'notFound' },
|
||||||
|
});
|
||||||
|
expect(res.written).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('image/png wird gestreamt; mit Version einen Tag gecacht, ohne eine Stunde', async () => {
|
it('image/png wird gestreamt; mit Version einen Tag gecacht, ohne eine Stunde', async () => {
|
||||||
const png = { status: 200, text: 'PNG', headers: { 'content-type': 'image/png' } };
|
const png = { status: 200, text: 'PNG', headers: { 'content-type': 'image/png' } };
|
||||||
const withV = setup(png);
|
const withV = setup(png);
|
||||||
|
|||||||
@@ -134,7 +134,12 @@ export class NextcloudFilesService {
|
|||||||
'x-content-type-options': 'nosniff',
|
'x-content-type-options': 'nosniff',
|
||||||
'content-security-policy': "default-src 'none'; sandbox",
|
'content-security-policy': "default-src 'none'; sandbox",
|
||||||
},
|
},
|
||||||
accept: (contentType) => contentType.toLowerCase().startsWith('image/'),
|
// Nur Rasterbilder (IN-02): SVG kann Skript enthalten; CSP-Sandbox und nosniff entschaerfen
|
||||||
|
// das zwar, aber eine Vorschau braucht kein SVG (Nextcloud rendert Vorschauen als PNG/JPEG).
|
||||||
|
accept: (contentType) => {
|
||||||
|
const type = contentType.toLowerCase();
|
||||||
|
return type.startsWith('image/') && !type.startsWith('image/svg');
|
||||||
|
},
|
||||||
maxBytes: PREVIEW_MAX_BYTES,
|
maxBytes: PREVIEW_MAX_BYTES,
|
||||||
onExpired: () => this.account.markExpired(tenantId, userId),
|
onExpired: () => this.account.markExpired(tenantId, userId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ describe('buildNcUrl / encodeSegments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Pfade', () => {
|
describe('Pfade', () => {
|
||||||
|
it('ein einzelnes UTF-16-Ersatzzeichen ist invalidPath statt eines URIError (WR-09)', () => {
|
||||||
|
expect(codeOf(() => validateSegment('a\uD800b'))).toBe('invalidPath');
|
||||||
|
expect(codeOf(() => validateSegment('\uDC00'))).toBe('invalidPath');
|
||||||
|
expect(codeOf(() => parseUserPath('/ok/\uD83D'))).toBe('invalidPath');
|
||||||
|
expect(codeOf(() => validateNewName('x\uDFFF'))).toBe('invalidName');
|
||||||
|
// ein vollstaendiges Paar (Emoji) bleibt erlaubt
|
||||||
|
expect(validateSegment('Foto \uD83D\uDE00.jpg')).toBe('Foto 😀.jpg');
|
||||||
|
expect(encodeSegments(['😀'])).toBe('%F0%9F%98%80');
|
||||||
|
});
|
||||||
|
|
||||||
it('parseUserPath: Wurzel und normale Pfade', () => {
|
it('parseUserPath: Wurzel und normale Pfade', () => {
|
||||||
expect(parseUserPath('')).toEqual([]);
|
expect(parseUserPath('')).toEqual([]);
|
||||||
expect(parseUserPath('/')).toEqual([]);
|
expect(parseUserPath('/')).toEqual([]);
|
||||||
|
|||||||
@@ -104,9 +104,14 @@ const MAX_SEGMENT_BYTES = 255;
|
|||||||
const MAX_SEGMENTS = 100;
|
const MAX_SEGMENTS = 100;
|
||||||
const MAX_PATH_CHARS = 4096;
|
const MAX_PATH_CHARS = 4096;
|
||||||
|
|
||||||
|
/** Ein einzelnes UTF-16-Ersatzzeichen ohne Partner (z. B. `"\uD800"` aus einem JSON-Koerper). */
|
||||||
|
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein einzelnes Pfadsegment pruefen (D-H): nicht leer, nicht `.`/`..`, kein
|
* Ein einzelnes Pfadsegment pruefen (D-H): nicht leer, nicht `.`/`..`, kein
|
||||||
* `/`, `\`, NUL oder Steuerzeichen, hoechstens 255 UTF-8-Byte.
|
* `/`, `\`, NUL oder Steuerzeichen, kein einzelnes Ersatzzeichen (WR-09: daran
|
||||||
|
* scheitert `encodeURIComponent` mit einem URIError, der sonst als 500 endete),
|
||||||
|
* hoechstens 255 UTF-8-Byte.
|
||||||
*/
|
*/
|
||||||
export function validateSegment(segment: string): string {
|
export function validateSegment(segment: string): string {
|
||||||
if (
|
if (
|
||||||
@@ -116,6 +121,7 @@ export function validateSegment(segment: string): string {
|
|||||||
segment === '..' ||
|
segment === '..' ||
|
||||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: Steuerzeichen sind hier gerade der Prueffall
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: Steuerzeichen sind hier gerade der Prueffall
|
||||||
/[\\/\u0000-\u001f\u007f]/.test(segment) ||
|
/[\\/\u0000-\u001f\u007f]/.test(segment) ||
|
||||||
|
LONE_SURROGATE.test(segment) ||
|
||||||
Buffer.byteLength(segment, 'utf8') > MAX_SEGMENT_BYTES
|
Buffer.byteLength(segment, 'utf8') > MAX_SEGMENT_BYTES
|
||||||
) {
|
) {
|
||||||
throw ncErrorDefault('invalidPath');
|
throw ncErrorDefault('invalidPath');
|
||||||
|
|||||||
@@ -497,6 +497,9 @@ describe('isRetryable', () => {
|
|||||||
expect(isRetryable(e(503, 'nextcloudMaintenance'))).toBe(false);
|
expect(isRetryable(e(503, 'nextcloudMaintenance'))).toBe(false);
|
||||||
expect(isRetryable(e(409, 'nameTaken'))).toBe(false);
|
expect(isRetryable(e(409, 'nameTaken'))).toBe(false);
|
||||||
expect(isRetryable(e(413, 'chunkTooLarge'))).toBe(false);
|
expect(isRetryable(e(413, 'chunkTooLarge'))).toBe(false);
|
||||||
|
// WR-09: ein ungueltiger Pfad ist jetzt 400 invalidPath — keine 4xx wird wiederholt.
|
||||||
|
expect(isRetryable(e(400, 'invalidPath'))).toBe(false);
|
||||||
|
expect(isRetryable(e(400, null))).toBe(false);
|
||||||
expect(isRetryable(e(0, 'aborted'))).toBe(false);
|
expect(isRetryable(e(0, 'aborted'))).toBe(false);
|
||||||
expect(isRetryable(new Error('x'))).toBe(false);
|
expect(isRetryable(new Error('x'))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user