Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent invalid roots to be defined #577

Merged
merged 2 commits into from Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 13 additions & 1 deletion __tests__/files/file.spec.ts
Expand Up @@ -81,7 +81,7 @@ describe('File creation', () => {
// path checks
expect(file.basename).toBe('picture.jpg')
expect(file.extension).toBe('.jpg')
expect(file.dirname).toBe('https://domain.com/Photos')
expect(file.dirname).toBe('/Photos')
expect(file.root).toBeNull()
expect(file.isDavRessource).toBe(false)
expect(file.permissions).toBe(Permission.READ)
Expand Down Expand Up @@ -137,6 +137,18 @@ describe('File data change', () => {
expect(file.mtime?.getDate()).toBe(new Date().getDate())
})

test('Moving a file to an invalid destination throws', () => {
const file = new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)),
})
expect(() => {
file.move('ftp://cloud.domain.com/remote.php/dav/files/emma/Pictures/picture-old.jpg')
}).toThrowError('Invalid source format, only http(s) is supported')
})

test('Moving a file to a different folder with root', () => {
const file = new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
Expand Down
4 changes: 2 additions & 2 deletions __tests__/files/folder.spec.ts
Expand Up @@ -2,7 +2,7 @@ import { Folder } from '../../lib/files/folder'
import { FileType } from '../../lib/files/fileType'
import { Permission } from '../../lib/permissions'

describe('File creation', () => {
describe('Folder creation', () => {
test('Valid dav folder', () => {
const folder = new Folder({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/',
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('File creation', () => {
// path checks
expect(folder.basename).toBe('Photos')
expect(folder.extension).toBeNull()
expect(folder.dirname).toBe('https://domain.com')
expect(folder.dirname).toBe('/')
expect(folder.root).toBeNull()
expect(folder.isDavRessource).toBe(false)
expect(folder.permissions).toBe(Permission.READ)
Expand Down
79 changes: 67 additions & 12 deletions __tests__/files/node.spec.ts
Expand Up @@ -80,8 +80,12 @@ describe('Sanity checks', () => {
source: '/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma'
})).toThrowError('Invalid source')

})).toThrowError('Invalid source format, source must be a valid URL')
expect(() => new File({
source: 'ftp://remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma'
})).toThrowError('Invalid source format, only http(s) is supported')
})

test('Invalid mtime', () => {
Expand Down Expand Up @@ -153,12 +157,27 @@ describe('Sanity checks', () => {
owner: 'emma',
root: true as unknown as string,
})).toThrowError('Invalid root format')

expect(() => new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
root: 'https://cloud.domain.com/remote.php/dav/',
})).toThrowError('Root must start with a leading slash')

expect(() => new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
root: '/files/john',
})).toThrowError('Root must be part of the source')

expect(() => new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
root: '/remote.php/dav/files/emma',
})).toThrowError('The root must be relative to the service. e.g /files/emma')
})
})

Expand Down Expand Up @@ -213,43 +232,79 @@ describe('Dav service detection', () => {

describe('Root and paths detection', () => {
test('Unknown root', () => {
const file1 = new File({
const file = new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
})
expect(file1.root).toBe('/files/emma/Photos')
expect(file1.dirname).toBe('/')
expect(file.root).toBe('/files/emma/Photos')
expect(file.dirname).toBe('/')
})

test('Provided root dav service', () => {
const file1 = new File({
const file = new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
root: '/files/emma',
})
expect(file1.root).toBe('/files/emma')
expect(file1.dirname).toBe('/Photos')
expect(file.root).toBe('/files/emma')
expect(file.dirname).toBe('/Photos')
})

test('Root with ending slash is removed', () => {
const file1 = new File({
const file = new File({
source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg',
mime: 'image/jpeg',
owner: 'emma',
root: '/files/emma/',
})
expect(file1.root).toBe('/files/emma')
expect(file.root).toBe('/files/emma')
expect(file.dirname).toBe('/Photos')
expect(file.path).toBe('/Photos/picture.jpg')
})

test('Root and source are the same', () => {
const file1 = new File({
const folder = new Folder({
source: 'https://cloud.domain.com/remote.php/dav/files/emma',
owner: 'emma',
root: '/files/emma',
})
expect(folder.root).toBe('/files/emma')
expect(folder.dirname).toBe('/')
expect(folder.path).toBe('/')
})

test('Source contains a similar root path', () => {
const folder = new Folder({
source: 'https://domain.com/remote.php/dav/files/emma/files/emma',
owner: 'emma',
root: '/files/emma',
})
expect(folder.root).toBe('/files/emma')
expect(folder.dirname).toBe('/files')
expect(folder.path).toBe('/files/emma')

const file = new File({
source: 'https://domain.com/remote.php/dav/files/emma/files/emma.jpeg',
mime: 'image/jpeg',
owner: 'emma',
root: '/files/emma',
})
expect(file1.dirname).toBe('/')
expect(file.root).toBe('/files/emma')
expect(file.dirname).toBe('/files')
expect(file.path).toBe('/files/emma.jpeg')
})

test('Non dav ressource with undefined root', () => {
const file = new File({
source: 'https://domain.com/files/images/emma.jpeg',
mime: 'image/jpeg',
owner: 'emma',
})
expect(file.isDavRessource).toBe(false)
expect(file.root).toBe(null)
expect(file.dirname).toBe('/files/images')
expect(file.path).toBe('/files/images/emma.jpeg')
})
})
24 changes: 18 additions & 6 deletions lib/files/node.ts
Expand Up @@ -22,7 +22,7 @@
import { basename, extname, dirname } from 'path'
import { Permission } from '../permissions'
import { FileType } from './fileType'
import NodeData, { Attribute, validateData } from './nodeData'
import NodeData, { Attribute, isDavRessource, validateData } from './nodeData'


export abstract class Node {
Expand All @@ -32,7 +32,7 @@ export abstract class Node {

constructor(data: NodeData, davService?: RegExp) {
// Validate data
validateData(data)
validateData(data, davService || this._knownDavService)

this._data = data

Expand Down Expand Up @@ -88,9 +88,15 @@ export abstract class Node {
*/
get dirname(): string {
if (this.root) {
return dirname(this.source.split(this.root).pop() || '/')
// Using replace would remove all part matching root
const firstMatch = this.source.indexOf(this.root)
return dirname(this.source.slice(firstMatch + this.root.length) || '/')
}
return dirname(this.source)

// This should always be a valid URL
// as this is tested in the constructor
const url = new URL(this.source)
return dirname(url.pathname)
}

/**
Expand Down Expand Up @@ -160,7 +166,7 @@ export abstract class Node {
* Is this a dav-related ressource ?
*/
get isDavRessource(): boolean {
return this.source.match(this._knownDavService) !== null
return isDavRessource(this.source, this._knownDavService)
}

/**
Expand All @@ -184,7 +190,12 @@ export abstract class Node {
/**
* Get the absolute path of this object relative to the root
*/
get path(): string|null {
get path(): string {
if (this.root) {
// Using replace would remove all part matching root
const firstMatch = this.source.indexOf(this.root)
return this.source.slice(firstMatch + this.root.length) || '/'
}
return (this.dirname + '/' + this.basename).replace(/\/\//g, '/')
}

Expand All @@ -202,6 +213,7 @@ export abstract class Node {
* e.g. https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg
*/
move(destination: string) {
validateData({ ...this._data, source: destination }, this._knownDavService)
this._data.source = destination
this._data.mtime = new Date()
}
Expand Down
26 changes: 24 additions & 2 deletions lib/files/nodeData.ts
Expand Up @@ -20,6 +20,7 @@
*
*/

import { join } from "path"
import { Permission } from "../permissions"

export interface Attribute { [key: string]: any }
Expand Down Expand Up @@ -62,11 +63,15 @@ export default interface NodeData {
*/
root?: string
}

export const isDavRessource = function(source: string, davService: RegExp): boolean {
return source.match(davService) !== null
}

/**
* Validate Node construct data
*/
export const validateData = (data: NodeData) => {
export const validateData = (data: NodeData, davService: RegExp) => {
if ('id' in data && (typeof data.id !== 'number' || data.id < 0)) {
throw new Error('Invalid id type of value')
}
Expand All @@ -75,8 +80,14 @@ export const validateData = (data: NodeData) => {
throw new Error('Missing mandatory source')
}

try {
new URL(data.source)
} catch (e) {
throw new Error('Invalid source format, source must be a valid URL')
}

if (!data.source.startsWith('http')) {
throw new Error('Invalid source format')
throw new Error('Invalid source format, only http(s) is supported')
}

if ('mtime' in data && !(data.mtime instanceof Date)) {
Expand Down Expand Up @@ -121,4 +132,15 @@ export const validateData = (data: NodeData) => {
if (data.root && !data.root.startsWith('/')) {
throw new Error('Root must start with a leading slash')
}

if (data.root && !data.source.includes(data.root)) {
throw new Error('Root must be part of the source')
}

if (data.root && isDavRessource(data.source, davService)) {
const service = data.source.match(davService)![0]
if (!data.source.includes(join(service, data.root))) {
throw new Error('The root must be relative to the service. e.g /files/emma')
}
}
}