[Zope] WebDAV security

Phil Harris phil.harris@zope.co.uk
Sat, 8 Jan 2000 15:23:52 -0000


This is a multi-part message in MIME format.

------=_NextPart_000_0027_01BF59EC.5EA21E20
Content-Type: text/plain;
	charset="iso-8859-1"
Content-Transfer-Encoding: 7bit

WebDav uses http authentication (it should use a digest mechanism but some
webdav servers don't bother).

This means that any anonymous user has the same rights whether using webdav
or just browsing through a browser.

The story changes though when you try to write to the server via webdav, you
then need authentication.

The stuff returned to the webdav client should be the rendered version
unless you know how to get at the unrendered version and know the
password/username.

I may be preoven wrong about this but that's how it should work according to
the docs.

If you are interested in looking at webdav, have a look at the attached
files.

These are a davlib.py client library and a simple test framework.  Play with
them, and you'll see what I mean.

HTH

Phil
phil.harris@zope.co.uk
----- Original Message -----
From: "Joachim Werner" <joachim.werner@iuveno.de>
To: <zope@zope.org>
Sent: Saturday, January 08, 2000 11:29 AM
Subject: [Zope] WebDAV security


> I have got a local configuration with a Linux server currently running
Zope
> 2.1.1 for evaluation and some other machines, including a Win98 Notebook,
> connected to the server via Ethernet.
>
> The good thing is that I can use WebDAV to see and modify the Zope
> folders/objects from Internet Explorer 5.0 on the Win98 machine.
>
> The bad thing: There seems to be NO AUTHENTICATION needed to do this!
>
> I tried first on the Win98 itself with a local Zope. My guess was that it
> automatically accepts local connections as allowed. But when I "WebDAVed"
into
> the Zope on the Linux machine, it was accessible, too. O.K., my Win98 uses
a
> valid user/password on the Linux machine needed for Samba, so I tried with
> a different logon and Zope still was very open for WebDAV connections!
>
> Same with Win98 running in a VMWare box on another Linux machine!
>
> Is this a "feature" or a bug? If WebDAV was totally "open" by default,
this
> would be a major security issue!
>
> The Linux server machine is also running SQUID as a proxy server for the
other
> machines. Could that be part of the problem?
>
> If it is a security issue (tI think so), Is that problem still there in
Zope
> 2.1.2?
>
> Joachim Werner
>
> _______________________________________________
> Zope maillist  -  Zope@zope.org
> http://lists.zope.org/mailman/listinfo/zope
> **   No cross posts or HTML encoding!  **
> (Related lists -
>  http://lists.zope.org/mailman/listinfo/zope-announce
>  http://lists.zope.org/mailman/listinfo/zope-dev )
>

------=_NextPart_000_0027_01BF59EC.5EA21E20
Content-Type: text/plain;
	name="davtest.py"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename="davtest.py"

import davlib

davConnection=davlib.DAV(host,int(port))
davConnection.setauth('admin','forgotten')
result=davConnection.get('http://localhost:8080/index_html/document_src')
print result.read()

------=_NextPart_000_0027_01BF59EC.5EA21E20
Content-Type: text/plain;
	name="davlib.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
	filename="davlib.py"

#
# DAV client library
#
# Copyright (C) 1998-1999 Guido van Rossum. All Rights Reserved.
# Written by Greg Stein. Given to Guido. Licensed using the Python =
license.
#
# This module is maintained by Greg and is available at:
#    http://www.lyra.org/greg/python/davlib.py
#
# Since this isn't in the Python distribution yet, we'll use the CVS ID
# for tracking:
#   $Id: davlib.py,v 1.2 1999/11/07 13:11:05 gstein Exp $
#

import httplib
import string
import types
import mimetypes
import qp_xml
import base64

error =3D 'davlib.error'

INFINITY =3D 'infinity'
XML_DOC_HEADER =3D '<?xml version=3D"1.0" encoding=3D"utf-8"?>'
XML_CONTENT_TYPE =3D 'text/xml; charset=3D"utf-8"'

# block size for copying files up to the server
BLOCKSIZE =3D 16384


class HTTPConnectionAuth(httplib.HTTPConnection):
  def __init__(self, *args, **kw):
    apply(httplib.HTTPConnection.__init__, (self,) + args, kw)

    self.__username =3D None
    self.__password =3D None
    self.__nonce =3D None
    self.__opaque =3D None

  def setauth(self, username, password):
    self.__username =3D username
    self.__password =3D password

  def getreply(self):
    result =3D httplib.HTTPConnection.getreply(self)
    if result[0] !=3D 401 or not self.__username:
      return result

    response =3D result[2]
    challenges =3D response.getallmatchingheaders('www-authenticate')
    assert challenges, 'HTTP violation: 401 with no WWW-Authenticate =
hdr'

    ### fill in stuff here...
    return result


_textof =3D qp_xml.textof

def _parse_status(elem):
  text =3D _textof(elem)
  idx1 =3D string.find(text, ' ')
  idx2 =3D string.find(text, ' ', idx1+1)
  return int(text[idx1:idx2]), text[idx2+1:]

class _blank:
  def __init__(self, **kw):
    self.__dict__.update(kw)
class _propstat(_blank): pass
class _response(_blank): pass
class _multistatus(_blank): pass

def _extract_propstat(elem):
  ps =3D _propstat(prop=3D{}, status=3DNone, responsedescription=3DNone)
  for child in elem.children:
    if child.ns !=3D 'DAV:':
      continue
    if child.name =3D=3D 'prop':
      for prop in child.children:
        ps.prop[(prop.ns, prop.name)] =3D prop
    elif child.name =3D=3D 'status':
      ps.status =3D _parse_status(child)
    elif child.name =3D=3D 'responsedescription':
      ps.responsedescription =3D _textof(child)
    ### unknown element name

  return ps

def _extract_response(elem):
  resp =3D _response(href=3D[], status=3DNone, =
responsedescription=3DNone, propstat=3D[])
  for child in elem.children:
    if child.ns !=3D 'DAV:':
      continue
    if child.name =3D=3D 'href':
      resp.href.append(_textof(child))
    elif child.name =3D=3D 'status':
      resp.status =3D _parse_status(child)
    elif child.name =3D=3D 'responsedescription':
      resp.responsedescription =3D _textof(child)
    elif child.name =3D=3D 'propstat':
      resp.propstat.append(_extract_propstat(child))
    ### unknown child element

  return resp

def _extract_msr(root):
  if root.ns !=3D 'DAV:' or root.name !=3D 'multistatus':
    raise 'invalid response: <DAV:multistatus> expected'

  msr =3D _multistatus(responses=3D[ ], responsedescription=3DNone)

  for child in root.children:
    if child.ns !=3D 'DAV:':
      continue
    if child.name =3D=3D 'responsedescription':
      msr.responsedescription =3D _textof(child)
    elif child.name =3D=3D 'response':
      msr.responses.append(_extract_response(child))
    ### unknown child element

  return msr

def _extract_locktoken(root):
  if root.ns !=3D 'DAV:' or root.name !=3D 'prop':
    raise 'invalid response: <DAV:prop> expected'
  elem =3D root.find('lockdiscovery', 'DAV:')
  if not elem:
    raise 'invalid response: <DAV:lockdiscovery> expected'
  elem =3D elem.find('activelock', 'DAV:')
  if not elem:
    raise 'invalid response: <DAV:activelock> expected'
  elem =3D elem.find('locktoken', 'DAV:')
  if not elem:
    raise 'invalid response: <DAV:locktoken> expected'
  elem =3D elem.find('href', 'DAV:')
  if not elem:
    raise 'invalid response: <DAV:href> expected'
  return elem.textof()


class DAVResponse(httplib.HTTPResponse):
  def parse_multistatus(self):
    self.root =3D qp_xml.Parser().parse(self)
    self.msr =3D _extract_msr(self.root)

  def parse_lock_response(self):
    self.root =3D qp_xml.Parser().parse(self)
    self.locktoken =3D _extract_locktoken(self.root)


class DAV(HTTPConnectionAuth):

  response_class =3D DAVResponse
  __username=3DNone
  __password=3DNone

  def setauth(self,username=3DNone,password=3DNone):
    self.__username=3Dusername
    self.__password=3Dpassword

  def get(self, url):
    return self._request('GET', url)

  def head(self, url):
    return self._request('HEAD', url)

  def post(self, url, data=3D{ }, body=3DNone, extra_hdrs=3D{ }):
    headers =3D { }
    headers.update(extra_hdrs)

    assert body or data, "body or data must be supplied"
    assert not (body and data), "cannot supply both body and data"
    if data:
      body =3D ''
      for key, value in data.items():
        if type(value) =3D=3D types.ListType:
          for item in value:
            body =3D body + '&' + key + '=3D' + urllib.quote(str(item))
        else:
          body =3D body + '&' + key + '=3D' + urllib.quote(str(value))
      body =3D body[1:]
      headers['Content-Type'] =3D 'application/x-www-form-urlencoded'

    return self._request('POST', url, body, headers)

  def options(self, url=3D'*'):
    return self._request('OPTIONS', url)

  def trace(self, url):
    return self._request('TRACE', url)

  def put(self, url, contents, content_type=3DNone, content_enc=3DNone):
    if not content_type:
      if type(contents) is types.FileType:
        content_type, content_enc =3D =
mimetypes.guess_type(contents.name)
      else:
        content_type, content_enc =3D mimetypes.guess_type(url)
    headers =3D { }
    if content_type:
      headers['Content-Type'] =3D content_type
    if content_enc:
      headers['Content-Encoding'] =3D content_enc
    return self._request('PUT', url, contents, headers)

  def delete(self, url):
    return self._request('DELETE', url)

  def propfind(self, url, body=3DNone, depth=3DNone):
    extra_hdrs =3D { 'Content-Type' : XML_CONTENT_TYPE }
    if depth is not None:
      extra_hdrs['Depth'] =3D str(depth)
    return self._request('PROPFIND', url, body, extra_hdrs)

  def proppatch(self, url, body):
    extra_hdrs =3D { 'Content-Type' : XML_CONTENT_TYPE }
    return self._request('PROPPATCH', url, body, extra_hdrs)

  def mkcol(self, url):
    return self._request('MKCOL', url)

  def move(self, src, dst):
    return self._request('MOVE', src, extra_hdrs=3D{ 'Destination' : dst =
})

  def copy(self, src, dst, depth=3DNone):
    extra_hdrs =3D { 'Destination' : dst }
    if depth is not None:
      extra_hdrs['Depth'] =3D str(depth)
    return self._request('COPY', src, extra_hdrs=3Dextra_hdrs)

  def lock(self, url, owner=3D'', timeout=3DNone, depth=3DNone):
    extra_hdrs =3D { 'Content-Type' : XML_CONTENT_TYPE }
    if depth is not None:
      extra_hdrs['Depth'] =3D str(depth)
    if timeout is not None:
      extra_hdrs['Timeout'] =3D timeout
    body =3D XML_DOC_HEADER + \
           '<DAV:lockinfo xmlns:DAV=3D"DAV:">' + \
           '<DAV:lockscope><DAV:exclusive/></DAV:lockscope>' + \
           '<DAV:locktype><DAV:write/></DAV:locktype>' + \
           owner + \
           '</DAV:lockinfo>'
    return self._request('LOCK', url, body, extra_hdrs=3Dextra_hdrs)

  def unlock(self, url, locktoken):
    if locktoken[0] !=3D '<':
      locktoken =3D '<' + locktoken + '>'
    return self._request('UNLOCK', url, extra_hdrs=3D{'Lock-Token' : =
locktoken})

  ### the _request() method needs some more work for reconnects...
  ### (especially w/ regard to body values that are Files)
  def _request(self, method, url, body=3DNone, extra_hdrs=3D{}):
    "Internal method for sending a request."

    self.putrequest(method, url)

    if body:
      ### length broken for files...
      self.putheader('Content-Length', str(len(body)))
    for hdr, value in extra_hdrs.items():
      self.putheader(hdr, value)

    if self.__username and self.__password:
      self.putheader("AUTHORIZATION","Basic %s" % =
string.replace(base64.encodestring("%s:%s" % =
(self.__username,self.__password)),"\012",""))
    self.endheaders()

    if body:
      if type(body) is types.FileType:
        while 1:
          block =3D body.read(BLOCKSIZE)
          if not block:
            break
          self.send(block)
      else:
        self.send(body)

    errcode, errmsg, response =3D self.getreply()
    if errcode =3D=3D -1:
      raise error, (errmsg, response)

    response.errcode =3D errcode
    response.errmsg =3D errmsg

    return response


  #
  # Higher-level methods for typical client use
  #

  def allprops(self, url, depth=3DNone):
    return self.propfind(url, depth=3Ddepth)

  def propnames(self, url, depth=3DNone):
    body =3D XML_DOC_HEADER + \
           '<DAV:propfind =
xmlns:DAV=3D"DAV:"><DAV:propname/></DAV:propfind>'
    return self.propfind(url, body, depth)

  def getprops(self, url, *names, **kw):
    assert names, 'at least one property name must be provided'
    if kw.has_key('ns'):
      xmlns =3D ' xmlns:NS=3D"' + kw['ns'] + '"'
      ns =3D 'NS:'
      del kw['ns']
    else:
      xmlns =3D ns =3D ''
    if kw.has_key('depth'):
      depth =3D kw['depth']
      del kw['depth']
    else:
      depth =3D 0
    assert not kw, 'unknown arguments'
    body =3D XML_DOC_HEADER + \
           '<DAV:propfind xmlns:DAV=3D"DAV:"' + xmlns + '><DAV:prop><' + =
ns + \
           string.joinfields(names, '/><' + ns) + \
           '/></DAV:prop></DAV:propfind>'
    return self.propfind(url, body, depth)

  def delprops(self, url, *names, **kw):
    assert names, 'at least one property name must be provided'
    if kw.has_key('ns'):
      xmlns =3D ' xmlns:NS=3D"' + kw['ns'] + '"'
      ns =3D 'NS:'
      del kw['ns']
    else:
      xmlns =3D ns =3D ''
    assert not kw, 'unknown arguments'
    body =3D XML_DOC_HEADER + \
           '<DAV:propertyupdate xmlns:DAV=3D"DAV:"' + xmlns + \
           '><DAV:remove><DAV:prop><' + ns + \
           string.joinfields(names, '/><' + ns) + \
           '/></DAV:prop></DAV:remove></DAV:propertyupdate>'
    return self.proppatch(url, body)

  def setprops(self, url, *xmlprops, **props):
    assert xmlprops or props, 'at least one property must be provided'
    elems =3D string.joinfields(xmlprops, '')
    if props.has_key('ns'):
      xmlns =3D ' xmlns:NS=3D"' + props['ns'] + '"'
      ns =3D 'NS:'
      del props['ns']
    else:
      xmlns =3D ns =3D ''
    for key, value in props.items():
      if value:
        elems =3D '%s<%s%s>%s</%s%s>' % (elems, ns, key, value, ns, key)
      else:
        elems =3D '%s<%s%s/>' % (elems, ns, key)
    body =3D XML_DOC_HEADER + \
           '<DAV:propertyupdate xmlns:DAV=3D"DAV:"' + xmlns + \
           '><DAV:set><DAV:prop>' + \
           elems + \
           '</DAV:prop></DAV:set></DAV:propertyupdate>'
    return self.proppatch(url, body)

  def get_lock(self, url, owner=3D'', timeout=3DNone, depth=3DNone):
    response =3D self.lock(url, owner, timeout, depth)
    response.parse_lock_response()
    return response.locktoken

------=_NextPart_000_0027_01BF59EC.5EA21E20--