triplecore-logotriplecore
  Zurück zur Blog-Übersicht

Dynamische Kontaktformulare für statische Webseiten

Im Zeitalter der Cloud ist das Hosten einer statischen Website billiger, schneller und einfacher als das herkömmliche On-Premise Hosting, bei dem immer mindestens ein Server laufen und gewartet werden muss.

Aber: Grundsätzlich ist keine statische Website wirklich statisch. In den allermeisten Fällen findet man irgendwo ein Kontakformular, welches nicht ohne Backend-Funktionalität auskommt.

AWS bietet eine sehr einfache und kostengünstige Möglichkeit, statische Webseiten in einem S3-Bucket unter der eigenen Domain zu hosten. Man kann entweder eine neue Domain bei AWS Route 53 registrieren oder die eigene Domain auf Route 53 übertragen.

Und natürlich möchte man keinen Server hochfahren, um ein einfaches Kontaktformular auf seiner Webseite integrieren zu können. Allerdings ist das Kontaktformular trotzdem ein sehr kristisches Element auf der Seite, da dies eben auch nicht als Einfallstor für Spam-Bots dienen soll.

Glücklicherweise bietet AWS eine einfache Möglichkeit, dies komplett serverless umzusetzen. Wir können AWS Lambda mit Amazon API Gateway verwenden, um ein serverloses Backend zu erstellen und mit dem Amazon Simple Email Service (SES) eine E-Mail an das Unternehmen zu senden, wenn ein Kunde eine Anfrage oder ein Feedback sendet. Schauen wir uns an, wie das funktioniert:

Architektur-Flow

Wir gehen hier von einem sehr einfachen Static-Website Szenario aus, bei dem die Domain über Route53 registriert und die Website in einem S3-Bucket gehostet ist. Zusätzlich ist die Domain auch für AWS SES verifiziert.

Hier der Architektur-Flow für das Kontaktformular:

Architektur-Flow Kontaktformular

Die Informationen fließen in 3 einfachen Schritten:

  • Das Kontakformular sammelt alle Informationen über den Benutzer und sendet diese an einen Endpunkt vom API Gateway (Zum Schutz vor Spam-Bots und anderen Angreifern wird AWS WAF eingesetzt)
  • Das API Gateway leitet diese Informationen an eine Lambda Funktion weiter
  • Die Lambda Funktion generiert automatisch eine E-Mail und sendet diese über den Amazon Simple Email Service an eine konfigurierte E-Mail-Adresse des Unternehmens

Aufsetzen des API Gateways

Zuerst setzen wir das API Gateway auf, um einen Http-Endpunkt für unser Kontaktformular zur Verfügung zu stellen. Dieser Endpunkt leitet die Informationen anschließend an die Lambda-Funktion weiter, die wir als Nächstes erstellen werden. Wir nutzen CloudFormation und AWS SAM, um unsere Infrastruktur wiederverwendbar darzustellen. Dazu legen wir zunächst eine Resource für die API an:

1Api:
2Type: AWS::Serverless::Api
3Properties:
4Name: contact-form-api
5StageName: production
6DefinitionBody:
7openapi: '3.0.1'
8paths:
9/contact:
10post:
11requestBody:
12content:
13application/json:
14schema:
15$ref: '#/components/schemas/contactform'
16required: true
17x-amazon-apigateway-integration:
18type: 'aws_proxy'
19uri: arn:aws:apigateway:eu-central-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-central-1:123456:function:contact-form:live/invocations
20passthroughBehavior: 'when_no_match'
21httpMethod: 'POST'
22Models:
23ContactForm:
24type: object
25required:
26- mail
27- name
28properties:
29mail:
30type: string
31name:
32type: string
33appointment:
34type: string

Der POST-Endpunkt unserer API erwartet folgenden Request-Body:

1{
2"mail": "someone@triplecore.io",
3"name": "Someone",
4"appointment": "ASAP"
5}

Erstellen der Lambda Funktion

Der nächste Schritt besteht darin, eine Lambda-Funktion zu erstellen, die vom API-Gateway angesprochen wird und AWS SES nutzt, um eine E-Mail zu versenden. Die Lambda-Funktion sieht ungefähr so aus:

1 const aws = require('aws-sdk');
2 const ses = new aws.SES();
3
4 exports.handler = async (event) => {
5 const requestBody = JSON.parse(event.body);
6 if (!requestBody.name || !requestBody.mail) {
7 return {
8 statusCode: 400,
9 body: JSON.stringify({ error_message: 'Missing parameters' }),
10 };
11 }
12
13 const params = {
14 Destination: {
15 ToAddresses: [process.env.RECIPIENT],
16 },
17 Source: process.env.SENDER,
18 Template: process.env.TEMPLATE,
19 TemplateData: JSON.stringify(requestBody),
20 };
21
22 return ses
23 .sendTemplatedEmail(params)
24 .promise()
25 .then(() => {
26 return { statusCode: 200, body: JSON.stringify({}) };
27 })
28 .catch((err) => {
29 return {
30 statusCode: 500,
31 body: JSON.stringify({ error_message: err.message }),
32 };
33 });
34 };
Wir stellen der Lambda-Funktion drei Environment-Variablen zur Verfügung(TEMPLATE, SENDER, RECIPIENT). Diese werden über das Cloudformation Template befüllt.
Wichtig: Falls in eurem AWS Account der Production-Mode für SES deaktiviert ist, müssen die E-Mails für SENDER und RECIPIENT einzeln verifiziert werden oder es muss eine Freischaltung für die Domain erfolgen.

Als nächstes müssen wir unsere Lambda Funktion auch in unserem Cloudformation Template anlegen:

1ContactFormLambda:
2Type: AWS::Serverless::Function
3Properties:
4Description: Lambda function to send email notifications for our contact form
5CodeUri: contact-form
6Handler: index.handler
7FunctionName: contact-form
8Runtime: 'nodejs12.x'
9Role: !GetAtt ContactFormLambdaIamRole.Arn
10Events:
11ApiEvent:
12Type: Api
13Properties:
14Path: /contact
15Method: post
16RestApiId: !Ref Api
17RequestModel:
18Model: ContactForm
19Required: true
20RequestParameters:
21- method.request.querystring.error
22Environment:
23Variables:
24RECIPIENT: info@example.com
25SENDER: info@example.com
26TEMPLATE: contact-form-template
27AutoPublishAlias: live
28DeploymentPreference:
29Type: AllAtOnce
30ContactFormLambdaIamRole:
31Type: AWS::IAM::Role
32Properties:
33AssumeRolePolicyDocument:
34Version: 2012-10-17
35Statement:
36- Effect: Allow
37Principal:
38Service:
39- lambda.amazonaws.com
40Action:
41- sts:AssumeRole
42RoleName: contact-form
43Policies:
44- PolicyName: contact-form
45PolicyDocument:
46Version: 2012-10-17
47Statement:
48- Effect: Allow
49Action:
50- ses:SendTemplatedEmail
51- logs:*
52Resource: '*'

Zusätzlich zur Lambda-Funktion legen wir auch noch eine IAM-Rolle an. Diese Rolle bekommt zwei Permissions, eine zum Senden der Email und die andere um Logs in Cloudwatch zu erzeugen. Der Code für die Lambda-Funktion liegt in einem Unterordner unter contact-form. Im Template referenzieren wir dann über CodeUri auf diesen Ordner.

Konfigurieren von AWS SES und anlegen des Email-Templates

Wie bereits im letzten Abschnitt erwähnt, müssen für SES die Sender und Empfänger Email-Adressen oder Domains verifiziert werden. Alternativ kann auch der Production-Mode über den Support angefragt werden. Dies sollte allerdings nur gemacht werden, wenn wirklich benötigt. Für unseren Fall reicht der Sandbox-Mode völlig aus. Weitere Infos dazu findet ihr in der offiziellen Dokumentation

Als nächsten Schritt wollen wir nun ein Template für unsere Email anlegen. Leider unterstützt Cloudformation momentan noch nicht das Anlegen von Template in eu-central-1. Aus diesem Grund müssen wir das Template manuell anlegen.

Im einfachsten Fall sieht das Template folgendermaßen aus:

1Template:
2HtmlPart: '<h1>Neue Kontaktanfrage</h1><p>Name: {{name}}</p><>Mail: {{mail}}</p>'
3SubjectPart: 'Neue Kontaktanfrage'
4TemplateName: 'website-api-contact-form'
5TextPart: 'Neue Kontaktanfrage: Name: {{name}} Mail: {{mail}}'

Das Template kann anschließend über die AWS-CLI angelegt werden:

1aws ses create-template --region eu-central-1 --cli-input-yaml file://email-template.yml

Absichern des Endpunkts über AWS WAF (Web-Application-Firewall)

Niemand mag Captcha´s und oftmals erhöhen sie auch die Absprungrate deutlich. Und auch wenn Google mit dem Invisible ReCaptcha eine Verbesserung geschaffen hat, möchte man das seinen Nutzern (auch aus datenschutzrechlicher Sicht) nicht zwingend zumuten. Jedoch ist es eine noch viele schlechtere Idee, den Endpunkt einfach so und ungeschützt ins Internet zu stellen. Eine einfache Lösung, hier mehr Sicherheit zu schaffen, ohne die Nutzererfahrung zu verschlechtern ist AWS WAF.

Dies ist eine Firewall für Webanwendungen, die wir vor unser API-Gateway schalten können, um HTTP- und HTTPS-Anfragen zu überwachen. Hiermit haben wir die Möglichkeit eigene Regeln zu definieren oder bereits von AWS konfigurierte Regeln zu nutzen.

AWS bietet bereits eine Reihe von verwalteten Regelgruppen an, die Schutz vor häufigen Anwendungsschwachstellen oder anderem unerwünschten Datenverkehr bieten, ohne dass wir eigene Regeln schreiben müssen.

Für unseren Use-Case verwenden wir die folgenden 4 Managed Rules:

  1. Core Rule Set

Das Core Rule Set enthält Regeln, die allgemein für Webanwendungen gelten. Dies bietet Schutz vor Ausnutzung einer Vielzahl von Schwachstellen, die in OWASP-Publikationen erwähnt werden. Diese Regel sollte für jeden WAF Use-Case genutzt werden.

  1. Known Bad Inputs

Die Regelgruppe "Known Bad Inputs" enthält Regeln zum Blockieren von Request-Mustern, die bekanntermaßen ungültig sind. Dies kann dazu beitragen, das Risiko zu verringern, dass ein Angreifer eine gefährdete Anwendung entdeckt.

  1. IP Reputation List

Diese Gruppe enthält Regeln, die auf interner Threat Intelligence von Amazon basieren. Dies ist hilfreich, wenn man IP-Adressen blockieren möchten, die typischerweise mit Bots oder anderen Bedrohungen verbunden sind. Das Blockieren dieser IP-Adressen kann dazu beitragen, Bots zu minimieren.

  1. Anonymous IP List

Die Regelgruppe „Anonymous IP List“ enthält Regeln für das Blockieren von Anfragen über Services, die die Verschleierung der Viewer-Identität ermöglichen. Dazu gehören Anfragen über VPNs, Proxys, Tor-Knoten und Hosting-Anbieter (einschließlich AWS). Diese Regelgruppe ist nützlich, wenn Sie Betrachter herausfiltern möchten, die möglicherweise versuchen, ihre Identität vor Ihrer Anwendung zu verbergen. Das Blockieren der IP-Adressen dieser Services kann dazu beitragen, Bots und Möglichkeiten zur Umgehung geografischer Einschränkungen zu minimieren.

Zusätzlich zu den von AWS Managed Rules wollen wir eine zusätzliche ratenbasierte Regel selbst definieren. Diese Regel zählt alle Anfragen an unseren POST-Endpunkt und blockiert IP-Adressen, die mehr als 100mal innerhalb von 5 Minuten unseren Endpunkt aufzurufen. Für unseren Fall sollte das völlig ausreichen. Es ist jedoch auch möglich noch weitere Einschränkungen vorzunehmen, bspw. könnte man auch bestimmte Geographische Regionen auschließen.

Die Konfiguration für die WAF haben wir folgendermaßen in Cloudformation angelegt:

1WAFWebACL:
2Type: AWS::WAFv2::WebACL
3Properties:
4Description: WebACL for website api
5DefaultAction:
6Allow: {}
7Name: web-acl
8Scope: REGIONAL
9VisibilityConfig:
10CloudWatchMetricsEnabled: true
11MetricName: web-acl
12SampledRequestsEnabled: true
13Rules:
14- Name: amazon-ip-reputation-list
15OverrideAction:
16None: {}
17Priority: 0
18Statement:
19ManagedRuleGroupStatement:
20Name: AWSManagedRulesAmazonIpReputationList
21VendorName: AWS
22VisibilityConfig:
23CloudWatchMetricsEnabled: true
24MetricName: amazon-ip-reputation-list
25SampledRequestsEnabled: true
26- Name: common-rule-set
27OverrideAction:
28None: {}
29Priority: 10
30Statement:
31ManagedRuleGroupStatement:
32Name: AWSManagedRulesCommonRuleSet
33VendorName: AWS
34VisibilityConfig:
35CloudWatchMetricsEnabled: true
36MetricName: common-rule-set
37SampledRequestsEnabled: true
38- Name: known-bad-inputs-rule-set
39OverrideAction:
40None: {}
41Priority: 20
42Statement:
43ManagedRuleGroupStatement:
44Name: AWSManagedRulesKnownBadInputsRuleSet
45VendorName: AWS
46VisibilityConfig:
47CloudWatchMetricsEnabled: true
48MetricName: known-bad-inputs-rule-set
49SampledRequestsEnabled: true
50- Name: anonymous-ip-list
51OverrideAction:
52None: {}
53Priority: 30
54Statement:
55ManagedRuleGroupStatement:
56Name: AWSManagedRulesAnonymousIpList
57VendorName: AWS
58VisibilityConfig:
59CloudWatchMetricsEnabled: true
60MetricName: anonymous-ip-list
61SampledRequestsEnabled: true
62- Name: contact-form-rule
63Action:
64Block: {}
65Priority: 40
66Statement:
67RateBasedStatement:
68Limit: 100
69AggregateKeyType: IP
70ScopeDownStatement:
71ByteMatchStatement:
72FieldToMatch:
73UriPath: {}
74PositionalConstraint: EXACTLY
75SearchString: '/contact'
76TextTransformations:
77- Type: NONE
78Priority: 0
79VisibilityConfig:
80CloudWatchMetricsEnabled: true
81MetricName: contact-form-rule
82SampledRequestsEnabled: true
83APIWebACLAssociation:
84DependsOn:
85- WAFWebACL
86- Api
87Type: AWS::WAFv2::WebACLAssociation
88Properties:
89ResourceArn: arn:aws:apigateway:eu-central-1::/restapis/api-id/stages/production
90WebACLArn: !GetAtt WAFWebACL.Arn

Fazit

Hier adressieren wir einen allgemeinen Anwendungsfall - ein einfaches Kontaktformular -, welches für jedes kleine Unternehmen wichtig ist, das seine Website auf Amazon S3 hostet. Dieser Beitrag soll dazu beitragen, Ihre statische Website dynamischer zu gestalten, ohne einen Server hochzufahren und gleichzeitig auch ein gewisses Maß an Sicherheit zu implementieren.

Julienne Schreiber - 31.8.2020