Core Rule Set: Nextcloud's calendar and contacts sync pass (closes #280)
check / check (push) Waiting to run
check / check (push) Waiting to run
rproxy passes MKCALENDAR on. nextcloud.conf allows it on /remote.php/dav/calendars/ and PROPFIND on /.well-known/caldav and carddav. The XML of an MKCOL there skips 930120, 932120, 932250 and 932260. That of an MKCALENDAR, and of an MKCOL on the address books' paths, also skips 932125 and 932230, for the description. A PUT of text/calendar or text/vcard there skips 920530: the Calendar and Contacts apps name the charset twice when they change an event or a contact. Recorded from Nextcloud 35.0.2 with Calendar 6.6.5, Contacts 8.9.5 and requests imitating DAVx5. Model: opus-5-5
This commit is contained in:
1 parent
463a19ef2a
commit
1dbbe01a94
6 files changed
+307
-7
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -126,8 +127,55 @@ var nextcloudBodies = []nextcloudBody{
|
||||
`<d:prop><d:displayname/></d:prop><d:literal>%%s%</d:literal>` +
|
||||
`</d:like></d:where></d:basicsearch></d:searchrequest>`,
|
||||
},
|
||||
{nextcloudMkcol, nextcloudCalendars + "work", xmlBody, nextcloudNewCollection},
|
||||
{nextcloudMkcol, nextcloudAddressBooks + "team", xmlBody, nextcloudNewCollection},
|
||||
{nextcloudMkcalendar, nextcloudCalendars + "work/", xmlBody, nextcloudNewCalendar},
|
||||
}
|
||||
|
||||
// The WebDAV methods that make a calendar or an address book.
|
||||
const (
|
||||
nextcloudMkcol = "MKCOL"
|
||||
nextcloudMkcalendar = "MKCALENDAR"
|
||||
)
|
||||
|
||||
// nextcloudCalendars and nextcloudAddressBooks hold the calendars and the
|
||||
// address books of the account admin.
|
||||
const (
|
||||
nextcloudCalendars = "/remote.php/dav/calendars/admin/"
|
||||
nextcloudAddressBooks = "/remote.php/dav/addressbooks/users/admin/"
|
||||
)
|
||||
|
||||
// nextcloudNewCollection is the XML body that names a new calendar or address
|
||||
// book, as an MKCOL sends it.
|
||||
const nextcloudNewCollection = `<d:mkcol xmlns:d="DAV:"><d:set><d:prop>` +
|
||||
`<d:displayname>%s</d:displayname></d:prop></d:set></d:mkcol>`
|
||||
|
||||
// nextcloudNewCalendar is the XML body of an MKCALENDAR that names the new
|
||||
// calendar.
|
||||
const nextcloudNewCalendar = `<c:mkcalendar xmlns:d="DAV:" ` +
|
||||
`xmlns:c="urn:ietf:params:xml:ns:caldav"><d:set><d:prop>` +
|
||||
`<d:displayname>%s</d:displayname></d:prop></d:set></c:mkcalendar>`
|
||||
|
||||
// nextcloudCalendarDescription is the XML body of an MKCALENDAR that gives
|
||||
// the new calendar a description.
|
||||
const nextcloudCalendarDescription = `<c:mkcalendar xmlns:d="DAV:" ` +
|
||||
`xmlns:c="urn:ietf:params:xml:ns:caldav"><d:set><d:prop>` +
|
||||
`<d:displayname>Meetings</d:displayname>` +
|
||||
`<c:calendar-description>%s</c:calendar-description>` +
|
||||
`</d:prop></d:set></c:mkcalendar>`
|
||||
|
||||
// nextcloudAddressBookDescription is the XML body of an MKCOL that gives a new
|
||||
// address book a description.
|
||||
const nextcloudAddressBookDescription = `<mkcol xmlns="DAV:" ` +
|
||||
`xmlns:CARD="urn:ietf:params:xml:ns:carddav"><set><prop>` +
|
||||
`<resourcetype><collection /><CARD:addressbook /></resourcetype>` +
|
||||
`<displayname>Trainers</displayname>` +
|
||||
`<CARD:addressbook-description>%s</CARD:addressbook-description>` +
|
||||
`</prop></set></mkcol>`
|
||||
|
||||
// nextcloudDescription is a calendar's description, as recorded.
|
||||
const nextcloudDescription = "Powershell and python3 meetings; ls -la first"
|
||||
|
||||
// nextcloudDocument is the text of a document the text editor saves, which
|
||||
// skips the rules for code blocks too.
|
||||
//
|
||||
@@ -375,6 +423,119 @@ func TestNextcloudWebDAVMethodsAreAllowedOnlyThere(t *testing.T) {
|
||||
}
|
||||
|
||||
wantResult(t, crs, request{"FOO", nextcloudFiles, nil}, matched(911100))
|
||||
|
||||
// MKCALENDAR only where the calendars are.
|
||||
for _, target := range []string{nextcloudCalendars, nextcloudCalendars + "work/"} {
|
||||
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched())
|
||||
}
|
||||
|
||||
for _, target := range []string{
|
||||
nextcloudFiles, nextcloudAddressBooks, "/remote.php/dav/",
|
||||
"/x/remote.php/dav/calendars/",
|
||||
} {
|
||||
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched(911100))
|
||||
}
|
||||
|
||||
// PROPFIND, and no other WebDAV method, where a client asks for them.
|
||||
for _, target := range []string{"/.well-known/caldav", "/.well-known/carddav"} {
|
||||
wantResult(t, crs, request{nextcloudPropFind, target, nil}, matched())
|
||||
wantResult(t, crs, request{nextcloudMkcol, target, nil}, matched(911100))
|
||||
}
|
||||
|
||||
other := request{nextcloudPropFind, "/.well-known/other", nil}
|
||||
wantResult(t, crs, other, matched(911100))
|
||||
}
|
||||
|
||||
func TestNextcloudDescriptionsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
// A new calendar's and a new address book's description, as recorded.
|
||||
for _, described := range []struct {
|
||||
sent nextcloudBody
|
||||
description string
|
||||
}{
|
||||
{
|
||||
nextcloudBody{
|
||||
nextcloudMkcalendar, nextcloudCalendars + "work/", xmlBody,
|
||||
nextcloudCalendarDescription,
|
||||
},
|
||||
nextcloudDescription,
|
||||
},
|
||||
{
|
||||
nextcloudBody{
|
||||
nextcloudMkcol, nextcloudAddressBooks + "trainers/", xmlBody,
|
||||
nextcloudAddressBookDescription,
|
||||
},
|
||||
"Powershell and python3 contacts; ls -la first",
|
||||
},
|
||||
} {
|
||||
sent := described.sent
|
||||
|
||||
request, score := nextcloudBodyScore(t, crs, sent, described.description)
|
||||
if score >= nextcloudThreshold {
|
||||
t.Errorf("%s refused: %d", request, score)
|
||||
}
|
||||
|
||||
for _, attack := range []string{
|
||||
sqlInjection, "..%2F..%2Fconfig%2Fconfig.php", scriptInjection, commandInjection,
|
||||
} {
|
||||
written, err := url.QueryUnescape(attack)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
request, score := nextcloudBodyScore(t, crs, sent, written)
|
||||
if score < nextcloudThreshold {
|
||||
t.Errorf("%s let through: %d", request, score)
|
||||
}
|
||||
}
|
||||
|
||||
// An MKCOL on the calendars' paths skips only the rules for names.
|
||||
sent.method, sent.target = nextcloudMkcol, nextcloudCalendars+"work/"
|
||||
|
||||
request, score = nextcloudBodyScore(t, crs, sent, described.description)
|
||||
if score < nextcloudThreshold {
|
||||
t.Errorf("%s let through: %d", request, score)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nextcloudChangedEvent is the Content-Type of the PUT with which the
|
||||
// Calendar app changes an event.
|
||||
const nextcloudChangedEvent = "Content-Type: text/calendar; charset=UTF-8; " +
|
||||
"component=vevent; charset=UTF-8"
|
||||
|
||||
func TestNextcloudChangedEventsAndContactsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
for _, changed := range []request{
|
||||
{http.MethodPut, nextcloudCalendars + "work/1.ics", []string{nextcloudChangedEvent}},
|
||||
{
|
||||
http.MethodPut, nextcloudAddressBooks + "team/1.vcf",
|
||||
[]string{"Content-Type: text/vcard; charset=UTF-8; charset=UTF-8"},
|
||||
},
|
||||
} {
|
||||
wantResult(t, crs, changed, matched())
|
||||
}
|
||||
|
||||
// Elsewhere, with another method or another type, two charsets are refused.
|
||||
for _, refused := range []request{
|
||||
{http.MethodPut, nextcloudFiles + "1.ics", []string{nextcloudChangedEvent}},
|
||||
{http.MethodPost, nextcloudCalendars + "work/1.ics", []string{nextcloudChangedEvent}},
|
||||
{
|
||||
http.MethodPut, nextcloudCalendars + "work/1.ics",
|
||||
[]string{"Content-Type: text/plain; charset=UTF-8; charset=UTF-8"},
|
||||
},
|
||||
} {
|
||||
if got := inspect(t, crs, refused); !slices.Contains(got.RuleIDs, 920530) {
|
||||
t.Errorf("%s %s %q let through: %+v",
|
||||
refused.method, refused.target, refused.headers, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nextcloudUploads are a JSON file and an XML file, as the Files app uploads
|
||||
|
||||
Reference in new issue
Block a user