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

Incorporate downstream CEGID changes. #13

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions web/ext/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,20 +253,20 @@ def prepare(self, context):

context.acl = ACL(context=context, policy=self.policy)

def dispatch(self, context, crumb):
def dispatch(self, context, path, handler, endpoint):
"""Called as dispatch descends into a tier.

The ACL extension uses this to build up the current request's ACL.
"""

acl = getattr(crumb.handler, '__acl__', ())
inherit = getattr(crumb.handler, '__acl_inherit__', True)
acl = getattr(handler, '__acl__', ())
inherit = getattr(handler, '__acl_inherit__', True)

if __debug__: log.debug(f"Handling dispatch event: {crumb.handler!r} {acl!r}", extra=dict(
if __debug__: log.debug(f"Handling dispatch event: {handler!r} {acl!r}", extra=dict(
request = id(context),
consumed = crumb.path,
handler = safe_name(crumb.handler),
endpoint = crumb.endpoint,
consumed = path,
handler = safe_name(handler),
endpoint = endpoint,
acl = [repr(i) for i in acl],
inherit = inherit,
))
Expand Down Expand Up @@ -301,6 +301,8 @@ def collect(self, context, handler, args, kw):
source = safe_name(grant.source) if grant.source else None
))

mutate = collect

def transform(self, context, handler, result):
try:
acl = result.__acl__
Expand Down
49 changes: 36 additions & 13 deletions web/ext/waf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

from abc import ABCMeta, abstractmethod
from html import escape
from ipaddress import ip_address
from re import compile as re
from socket import inet_aton
from socket import socket, AF_INET, SOCK_DGRAM

from typeguard import check_argument_types
from uri import URI
Expand All @@ -30,8 +31,8 @@

log = __import__('logging').getLogger(__name__) # A standard logger object.


ClientSet = MutableSet[bytes]
Remotes = Set[str]
ClientSet = MutableSet[int]

class PersistentClientSet(ClientSet, metaclass=ABCMeta):
"""A mutable set exposing two methods for persisting and restoring its contents."""
Expand All @@ -45,6 +46,26 @@ def restore(self, context:Context) -> None:
...


class MongoDBPersistentClientSet(PersistentClientSet):
"""Store blacklists as MongoDB records."""

def persist(self, context:Context) -> None:
pass

def restore(self, context:Context) -> None:
self.clear()
self.update()


def current_ip(self):
s = socket(AF_INET, SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
default_route = s.getsockname()[0]
s.close()

return default_route


class WebApplicationFirewallExtension:
"""A basic rules-based Web Application Firewall implementation."""

Expand All @@ -56,9 +77,10 @@ class WebApplicationFirewallExtension:

heuristics:Iterable[WAFHeuristic] # The prepared heuristic instances.
blacklist:ClientSet # The current blacklist. Can theoretically be swapped for any mutable set-like object.
exempt:ClientSet # IP addresses exempt from blacklisting.
exempt:ClientSet # IP addresses exempt from blacklisting. Defaults to 127.0.0.1 and the IP of default interface.

def __init__(self, *heuristics, blacklist:Optional[ClientSet]=None, exempt:Optional[ClientSet]=None) -> None:
def __init__(self, *heuristics, blacklist:Optional[Remotes]=None,
exempt:Optional[Remotes]={'127.0.0.1', current_ip()}) -> None:
"""Executed to configure the extension.

No actions must be performed here, only configuration management.
Expand All @@ -73,10 +95,10 @@ def __init__(self, *heuristics, blacklist:Optional[ClientSet]=None, exempt:Optio
self.heuristics = heuristics

# Permit custom backing stores to be passed in; we optimize by storing packed binary values, not strings.
self.blacklist = set() if blacklist is None else set(inet_aton(i) for i in blacklist)
self.blacklist = set(int(ip_address(i)) for i in blacklist) if blacklist else set()

# Permit custom backing stores to be passed in for the exemptions, as well.
self.exempt = set() if exempt is None else exempt
self.exempt = set(int(ip_address(i)) for i in exempt) if exempt else set()

def __call__(self, context:Context, app:WSGI) -> WSGI:
"""Wrap the WSGI application callable in our 'web application firewall'."""
Expand All @@ -87,17 +109,17 @@ def inner(environ:WSGIEnvironment, start_response:WSGIStartResponse):
try:
request: Request = Request(environ) # This will be remembered and re-used as a singleton later.
uri: URI = URI(request.url)
client: int = int(ip_address(request.client_addr))

# https://docs.pylonsproject.org/projects/webob/en/stable/api/request.html#webob.request.BaseRequest.client_addr
# Ref: https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/

except Exception as e: # Protect against de-serialization errors.
return HTTPBadRequest(f"Encountered error de-serializing the request: {e!r}")(environ, start_response)

# https://docs.pylonsproject.org/projects/webob/en/stable/api/request.html#webob.request.BaseRequest.client_addr
# Ref: https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
client: str = request.client_addr

try:
# Immediately reject known bad actors.
if inet_aton(request.client_addr) in self.blacklist:
if client in self.blacklist:
return HTTPClose()(environ, start_response) # No need to re-blacklist.

# Validate the heuristic rules.
Expand All @@ -115,7 +137,7 @@ def inner(environ:WSGIEnvironment, start_response:WSGIStartResponse):
except HTTPClose as e:
if request.client_addr not in self.exempt:
log.warning(f"Blacklisting: {request.client_addr}")
self.blacklist.add(inet_aton(request.client_addr))
self.blacklist.add(client)

if not __debug__: e = HTTPClose() # Do not disclose the reason in production environments.
elif ': ' in e.args[0]: # XXX: Not currently effective.
Expand Down Expand Up @@ -171,3 +193,4 @@ def plural(quantity, single, plural):

c = len(self.blacklist)
yield f"Blacklist: {c} {plural(c, 'entry', 'entries')}"