Welcome to the secure Google AppEngine scaffold application.
8 | These pages are meant to give you a brief tour of some of the security
9 | features of the scaffold. You can find more by reading the code,
10 | starting with the bundled main.py.
11 | In the meantime, though, click on the links below to find out a bit
12 | more about the security features in action.
13 |
8 | Run unit tests for App Engine apps.
9 |
10 | SDK_PATH Path to the SDK installation
11 | TEST_PATH Path to package containing test modules
12 | THIRD_PARTY Optional path to third party python modules to include."""
13 |
14 | def main(sdk_path, test_path, third_party_path=None):
15 | sys.path.insert(0, sdk_path)
16 | import dev_appserver
17 | dev_appserver.fix_sys_path()
18 | if third_party_path:
19 | sys.path.insert(0, third_party_path)
20 |
21 | try:
22 | import appengine_config
23 | (appengine_config)
24 | except ImportError:
25 | print "Note: unable to import appengine_config."
26 |
27 | suite = unittest2.loader.TestLoader().discover(test_path,
28 | pattern='*_test.py')
29 | result = unittest2.TextTestRunner(verbosity=2).run(suite)
30 | if len(result.errors) > 0 or len(result.failures) > 0:
31 | sys.exit(1)
32 |
33 |
34 | if __name__ == '__main__':
35 | sys.dont_write_bytecode = True
36 | parser = optparse.OptionParser(USAGE)
37 | options, args = parser.parse_args()
38 | if len(args) < 2:
39 | print 'Error: At least 2 arguments required.'
40 | parser.print_help()
41 | sys.exit(1)
42 | SDK_PATH = args[0]
43 | TEST_PATH = args[1]
44 | THIRD_PARTY_PATH = args[2] if len(args) > 2 else None
45 | main(SDK_PATH, TEST_PATH, THIRD_PARTY_PATH)
46 |
--------------------------------------------------------------------------------
/src/base/xsrf.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Utilities related to Cross-Site Request Forgery protection."""
15 |
16 | import hashlib
17 | import hmac
18 | import time
19 |
20 | DELIMITER_ = ':'
21 | DEFAULT_TIMEOUT_ = 86400
22 |
23 |
24 | def _Compare(a, b):
25 | """Compares a and b in constant time and returns True if they are equal."""
26 | if len(a) != len(b):
27 | return False
28 | result = 0
29 | for x, y in zip(a, b):
30 | result |= ord(x) ^ ord(y)
31 |
32 | return result == 0
33 |
34 |
35 | def GenerateToken(key, user, action='*', now=None):
36 | """Generates an XSRF token for the provided user and action."""
37 | token_timestamp = now or int(time.time())
38 | message = DELIMITER_.join([user, action, str(token_timestamp)])
39 | digest = hmac.new(key, message, hashlib.sha1).hexdigest()
40 | return DELIMITER_.join([str(token_timestamp), digest])
41 |
42 |
43 | def ValidateToken(key, user, token, action='*', max_age=DEFAULT_TIMEOUT_):
44 | """Validates the provided XSRF token."""
45 | if not token or not user:
46 | return False
47 | try:
48 | (timestamp, digest) = token.split(DELIMITER_)
49 | except ValueError:
50 | return False
51 | expected = GenerateToken(key, user, action, timestamp)
52 | (_, expected_digest) = expected.split(DELIMITER_)
53 | now = int(time.time())
54 | if _Compare(expected_digest, digest) and now < int(timestamp) + max_age:
55 | return True
56 | return False
57 |
--------------------------------------------------------------------------------
/src/base/api_fixer_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Tests for base.api_fixer."""
15 |
16 | import json
17 | import pickle
18 | import unittest2
19 | import yaml
20 |
21 | import api_fixer
22 |
23 |
24 | class BadPickle(object):
25 | """Dummy object."""
26 | def __reduce__(self):
27 | return tuple([eval, tuple(['[1][2]'])])
28 |
29 |
30 | class ApiFixerTest(unittest2.TestCase):
31 | """Test cases for base.api_fixer."""
32 |
33 | def testJsonEscaping(self):
34 | o = {'foo': ''}
35 | self.assertFalse('<' in json.dumps(o))
36 |
37 | def testYamlLoading(self):
38 | unsafe = '!!python/object/apply:os.system ["ls"]'
39 | try:
40 | yaml.load(unsafe)
41 | self.fail('loading unsafe YAML object succeeded')
42 | except yaml.constructor.ConstructorError:
43 | pass
44 |
45 | def testPickle(self):
46 | b = { 'foo': BadPickle() }
47 | s = pickle.dumps(b)
48 | try:
49 | b = pickle.loads(s)
50 | self.fail('BadPickle() loaded successfully')
51 | except IndexError:
52 | self.fail('pickled code execution')
53 | except api_fixer.ApiSecurityException:
54 | pass
55 |
56 | foo = { 'bar': [1, 2, 3] }
57 | s = pickle.dumps(foo)
58 | try:
59 | foo = pickle.loads(s)
60 | self.assertEqual(foo['bar'][0], 1)
61 | except Exception:
62 | self.fail('safe unpickling failed')
63 |
64 |
65 | if __name__ == '__main__':
66 | unittest2.main()
67 |
--------------------------------------------------------------------------------
/app.yaml.base:
--------------------------------------------------------------------------------
1 | ###########################################################################
2 | # DO NOT MODIFY THIS FILE WITHOUT UNDERSTANDING THE SECURITY IMPLICATIONS #
3 | ###########################################################################
4 |
5 | # The "application" parameter is automatically set based on the below rules:
6 | # For util.sh: the argument to '-p' is used
7 | # For Grunt users: the 'appid' value in config.json. It may also be
8 | # overriden by passing an '--appid=' parameter to grunt
9 | # You do not need to modify it here.
10 | application: __APPLICATION__
11 |
12 | # The version is automatically generated based on the current git hash.
13 | # If there are uncommitted changes, a '-dev' suffix will be added. You do
14 | # not need to modify it here.
15 | version: __VERSION__
16 | runtime: python27
17 | api_version: 1
18 | threadsafe: true
19 |
20 | handlers:
21 | - url: /static/
22 | static_dir: static/
23 | secure: always
24 | http_headers:
25 | X-Frame-Options: "DENY"
26 | Strict-Transport-Security: "max-age=2592000; includeSubdomains"
27 | X-Content-Type-Options: "nosniff"
28 | X-XSS-Protection: "1; mode=block"
29 |
30 | # All URLs should be mapped via the *_ROUTES variables in the src/main.py file.
31 | # See https://webapp-improved.appspot.com/guide/routing.html for information on
32 | # how URLs are routed in the webapp2 framework. Do not add additional handlers
33 | # directly here.
34 | - url: /.*
35 | script: main.app
36 | secure: always
37 |
38 | libraries:
39 | - name: django
40 | version: latest
41 |
42 | - name: jinja2
43 | version: latest
44 |
45 | - name: webapp2
46 | version: latest
47 |
48 | skip_files:
49 | - ^(.*/)?#.*#$
50 | - ^(.*/)?.*~$
51 | - ^(.*/)?.*\.py[co]$
52 | - ^(.*/)?.*/RCS/.*$
53 | - ^(.*/)?\..*$
54 | - app.yaml.base
55 | - README
56 | - util.sh
57 | - run_tests.py
58 | - .*_test.py
59 | - js/.*
60 | - closure-library/.*
61 |
--------------------------------------------------------------------------------
/src/base/xsrf_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Tests for base.xsrf."""
15 |
16 | import os
17 | import time
18 | import unittest2
19 |
20 | import xsrf
21 |
22 |
23 | class XsrfTest(unittest2.TestCase):
24 | """Test cases for base.xsrf."""
25 |
26 | def setUp(self):
27 | # non-deterministic tests FTW!
28 | self.key = os.urandom(16)
29 |
30 | def testCompare(self):
31 | self.assertTrue(xsrf._Compare('a', 'a'))
32 | self.assertFalse(xsrf._Compare('a', 'b'))
33 | self.assertFalse(xsrf._Compare('a', 'ab'))
34 |
35 | def testTokenWithNoActionVerifies(self):
36 | token = xsrf.GenerateToken(self.key, 'user')
37 | self.assertTrue(xsrf.ValidateToken(self.key, 'user', token))
38 |
39 | def testTokenWithDifferentActionsFail(self):
40 | token = xsrf.GenerateToken(self.key, 'user', 'a')
41 | self.assertFalse(xsrf.ValidateToken(self.key, 'user', token, 'b'))
42 |
43 | def testTokenWithDifferentUsersFail(self):
44 | token = xsrf.GenerateToken(self.key, 'user')
45 | self.assertFalse(xsrf.ValidateToken(self.key, 'otheruser', token))
46 |
47 | def testExpiredTokenDoesNotVerify(self):
48 | now = int(time.time()) - (xsrf.DEFAULT_TIMEOUT_ + 1)
49 | token = xsrf.GenerateToken(self.key, 'user', '*', now)
50 | self.assertFalse(xsrf.ValidateToken(self.key, 'user', token))
51 | self.assertTrue(xsrf.ValidateToken(self.key, 'user', token, '*',
52 | xsrf.DEFAULT_TIMEOUT_ * 2))
53 |
54 |
55 | if __name__ == '__main__':
56 | unittest2.main()
57 |
--------------------------------------------------------------------------------
/src/base/constants.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Public constants for use in application configuration."""
15 |
16 | import os
17 |
18 |
19 | def _IsDevAppServer():
20 | return os.environ.get('SERVER_SOFTWARE', '').startswith('Development')
21 |
22 | # CSP Nonce length
23 | NONCE_LENGTH = 10
24 |
25 | # webapp2 application configuration constants.
26 | # template
27 | (CLOSURE, DJANGO, JINJA2) = range(0, 3)
28 |
29 | # using_angular
30 | DEFAULT_ANGULAR = False
31 |
32 | # framing_policy
33 | (DENY, SAMEORIGIN, PERMIT) = range(0, 3)
34 | X_FRAME_OPTIONS_VALUES = {DENY: 'DENY', SAMEORIGIN: 'SAMEORIGIN'}
35 |
36 | # hsts_policy
37 | DEFAULT_HSTS_POLICY = {'max_age': 2592000, 'includeSubdomains': True}
38 |
39 | # placeholder for the CSP nonce. 'nonce_value' is replaced for every response
40 | # in base/handers.py with a random nonce value.
41 | CSP_NONCE_PLACEHOLDER_FORMAT = '\'nonce-%(nonce_value)s\' '
42 |
43 | # IS_DEV_APPSERVER is primarily used for decisions that rely on whether or
44 | # not the application is currently serving over HTTPS (dev_appserver does
45 | # not support HTTPS).
46 | IS_DEV_APPSERVER = _IsDevAppServer()
47 |
48 | DEBUG = IS_DEV_APPSERVER
49 |
50 | TEMPLATE_DIR = os.path.sep.join([os.path.dirname(__file__), '..', 'templates'])
51 |
52 | # csp_policy
53 | DEFAULT_CSP_POLICY = {
54 | # Restrict base tags to same origin, to prevent CSP bypasses.
55 | 'base-uri': '\'self\'',
56 | # Disallow Flash, etc.
57 | 'object-src': '\'none\'',
58 | # Strict CSP with fallbacks for browsers not supporting CSP v3.
59 | 'script-src': CSP_NONCE_PLACEHOLDER_FORMAT +
60 | # Propagate trust to dynamically created scripts.
61 | '\'strict-dynamic\' '
62 | # Fallback. Ignored in presence of a nonce
63 | '\'unsafe-inline\' '
64 | # Fallback. Ignored in presence of strict-dynamic.
65 | 'https: http:',
66 | 'report-uri': '/csp',
67 | 'reportOnly': DEBUG,
68 | }
69 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute #
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | a just a few small guidelines you need to follow.
5 |
6 |
7 | ## Contributor License Agreement ##
8 |
9 | Contributions to any Google project must be accompanied by a Contributor
10 | License Agreement. This is not a copyright **assignment**, it simply gives
11 | Google permission to use and redistribute your contributions as part of the
12 | project.
13 |
14 | * If you are an individual writing original source code and you're sure you
15 | own the intellectual property, then you'll need to sign an [individual
16 | CLA][].
17 |
18 | * If you work for a company that wants to allow you to contribute your work,
19 | then you'll need to sign a [corporate CLA][].
20 |
21 | You generally only need to submit a CLA once, so if you've already submitted
22 | one (even if it was for a different project), you probably don't need to do it
23 | again.
24 |
25 | [individual CLA]: https://developers.google.com/open-source/cla/individual
26 | [corporate CLA]: https://developers.google.com/open-source/cla/corporate
27 |
28 | Once your CLA is submitted (or if you already submitted one for
29 | another Google project), make a commit adding yourself to the
30 | [AUTHORS][] and [CONTRIBUTORS][] files. This commit can be part
31 | of your first [pull request][].
32 |
33 | [AUTHORS]: AUTHORS
34 | [CONTRIBUTORS]: CONTRIBUTORS
35 |
36 |
37 | ## Submitting a patch ##
38 |
39 | 1. It's generally best to start by opening a new issue describing the bug or
40 | feature you're intending to fix. Even if you think it's relatively minor,
41 | it's helpful to know what people are working on. Mention in the initial
42 | issue that you are planning to work on that bug or feature so that it can
43 | be assigned to you.
44 |
45 | 1. Follow the normal process of [forking][] the project, and setup a new
46 | branch to work in. It's important that each group of changes be done in
47 | separate branches in order to ensure that a pull request only includes the
48 | commits related to that bug or feature.
49 |
50 | 1. Do your best to have [well-formed commit messages][] for each change.
51 | This provides consistency throughout the project, and ensures that commit
52 | messages are able to be formatted properly by various git tools.
53 |
54 | 1. Finally, push the commits to your fork and submit a [pull request][].
55 |
56 | [forking]: https://help.github.com/articles/fork-a-repo
57 | [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
58 | [pull request]: https://help.github.com/articles/creating-a-pull-request
59 |
--------------------------------------------------------------------------------
/templates/csp.tpl:
--------------------------------------------------------------------------------
1 | {% extends "base.tpl" %}
2 | {% block title %}
3 | Content Security Policy
4 | {% endblock %}
5 | {% block content %}
6 | Content Security Policy
7 | CSP is a mechanism designed to make applications more secure against common
8 | Web vulnerabilities, particularly XSS.
9 | It is enabled by delivering a policy in the Content-Security-Policy HTTP response header.
10 |
11 |
12 | A production-quality strict policy appropriate for many products is:
13 |
14 | Content-Security-Policy:
15 | base-uri 'self';
16 | object-src 'none';
17 | script-src 'nonce-{random}' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' https: http:;
18 |
19 |
20 |
21 |
22 | When such a policy is set, modern browsers will execute only those scripts whose
23 | nonce attribute matches the value set in the policy header, as well as scripts
24 | dynamically added to the page by scripts with the proper nonce.
Older browsers,
25 | which don't support the CSP3 standard, will ignore the nonce-* and
26 | 'strict-dynamic' keywords and fall back to
27 | [script-src 'unsafe-inline' https: http:] which will not provide
28 | protection against XSS vulnerabilities, but will allow the application to
29 | function properly.
30 |
31 |
32 | Adopting a strict policy
33 |
34 | To use a strict CSP policy, most applications will need to make the following changes:
35 |
36 |
37 | - Add a nonce attribute to all
<script> elements. Some template systems can do this automatically.
38 |
E.g. in Jinja:
39 | {% raw %}
40 | <script> nonce="{{_csp_nonce}}" src="..."></script>
41 | {% endraw %}
42 |
43 | - Refactor any markup with inline event handlers (
onclick, etc.) and javascript: URIs (details).
44 | - For every page load, generate a new nonce, pass it the to the template system, and use the same value in the
Content-Security-Policy response header.
45 |
46 |
47 |
48 | Example of a nonced script that dynamically adds child scripts
49 |
55 |
56 |
57 |
58 | {% endblock %}
59 |
--------------------------------------------------------------------------------
/templates/xsrf.tpl:
--------------------------------------------------------------------------------
1 | {% extends "base.tpl" %}
2 | {% block title %}
3 | Cross-Site Request Forgery
4 | {% endblock %}
5 | {% block content %}
6 | Cross-Site Request Forgery
7 | Welcome, {{email}}! You may notice that you had to log in to visit this
8 | page. That's because this vulnerability is specific to authenticated
9 | functionality. As such, the class that powers this page extends from
10 | AuthenticatedHandler, which requires users to be logged in
11 | before dispatching the request.
12 |
13 | Cross-Site Request Forgery (XSRF) occurs when a web application performs
14 | some state changing action without requiring an unpredictable / unforgeable
15 | token before making the change. The canonical example is of a vulnerable
16 | online banking application that allows users to transfer funds when they visit
17 | a URL like: https://example-bank.com/transfer?to_acct=1234&amount=1000.
18 |
19 | If you're logged in to the bank, but browsing another web site, they could
20 | easily insert into their page:
21 |
22 | <img src="https://example-bank.com/transfer?to_account=1234&
23 | amount=1000" style="display:none" />
24 |
25 |
26 | In the background, your browser would make the request (and send your
27 | authentication cookies), and the transfer would succeed. Unfortunately,
28 | simply switching to POST requests does not help, because the evil web site
29 | could automatically submit a cross-domain form POST by using Javascript.
30 |
31 | The only solution that works is to include something that a third party
32 | site can't predict, and that isn't sent along automatically with the
33 | request (like cookies). This is commonly referred to as an XSRF token. The
34 | token should be included either as a form parameter, or embedded in an
35 | HTTP request header.
36 | The secure framework actually generates these tokens and augments the
37 | template variable dictionary you pass to render() with a
38 | valid XSRF token under the key _xsrf, which you can include
39 | in hidden form fields / HTTP request headers. This token MUST
40 | be present in POST requests to classes that extend from
41 | AuthenticatedHandler, AuthenticatedAjaxHandler, or
42 | AuthenticatedAdminHandler. It is verified before your
43 | implementation is called. If the token fails to validate, the
44 | XsrfFail() in your handler is invoked.
45 |
46 | This page implements a simple counter that counts how many times a
47 | valid request was processed. You can see the generated XSRF token below,
48 | feel free to modify it and see how that impacts the success / failure of
49 | request processing!
50 |
51 | Current value of counter: {{counter}}
52 | {% if xsrf_fail %}
53 | XSRF token validation failed!
54 | {% endif %}
55 |
56 |
61 |
62 | {% endblock %}
63 |
--------------------------------------------------------------------------------
/templates/soy/example.soy:
--------------------------------------------------------------------------------
1 | /*
2 | * Note that this namespace must be unique if this is used with the Closure
3 | * Library:
4 | * https://developers.google.com/closure/templates/docs/javascript_usage
5 | * Section: "Usage with the Closure Library"
6 | *
7 | * Strict mode autoescaping is the default in newer versions of the Closure
8 | * Template compiler, but we explicitly specify it for now.
9 | */
10 | {namespace example autoescape="strict"}
11 |
12 | {template .header}
13 | {@param title: string}
14 |
15 |
16 |
17 | {$title}
18 |
19 | {/template}
20 |
21 | {template .footer}
22 |
23 | {/template}
24 |
25 | {template .xss}
26 | {@param? s: string}
27 | {call .header}
28 | {param title: 'Cross-Site Scripting' /}
29 | {/call}
30 | Cross-Site Scripting
31 | Cross-Site Scripting (XSS) occurs when user input is output by a web server
32 | without being properly escaped for the context in which it is
33 | displayed.
34 |
35 | Consider a simple page like this one, which, when given a name, will
36 | print a simple greeting, e.g. "Hello, Jane!"
37 | What happens if a user decides to enter something malicious for their
38 | name, maybe something like:
39 | <script>alert(1);</script>
40 |
41 | If we output that string directly in our page, we would execute the
42 | (possibly malicious) Javascript! The problem gets even more complex when
43 | you consider that the escaping rules vary across contexts - e.g. escaping
44 | is done differently inside <script> blocks.
45 |
46 | The good news is that this project uses Closure Templates by default,
47 | which support strict contextually aware autoescaping - see the
49 | security documentation for more. There are still some risks (see
50 | the comments in src/main.py about mixing client and server
51 | side template languages) - but you can see how the value is escaped
52 | differently by viewing the source of this page. Try submitting the form
53 | below (the value will be printed in both HTML and Javascript contexts
54 | right below the form), and use various special characters to see how the
55 | escaping behavior differs.
56 |
57 |
63 | Output
64 | {if $s}{$s}{/if}
65 |
72 |
75 | {call .footer /}
76 | {/template}
77 |
78 |
--------------------------------------------------------------------------------
/templates/xssi.tpl:
--------------------------------------------------------------------------------
1 | {% extends "base.tpl" %}
2 | {% block title %}
3 | Cross-Site Script Inclusion
4 | {% endblock %}
5 | {% block content %}
6 | Cross-Site Script Inclusion
7 | Cross-Site Script Inclusion (XSSI) occurs when a web application returns
8 | a response containing non-public data that can be parsed and interpreted as
9 | Javascript. It very frequently happens in "Web 2.0" applications that make
10 | use of paradigms like JSONP.
11 | Consider the following request/response pairs for a hypothetical JSONP
12 | endpoint at "https://example.com/contacts?jsonp=foo" :
13 |
14 |
15 |
16 |
17 | GET /contacts?jsonp=foo HTTP/1.1
18 | Host: example.com
19 | Cookie: session_id=12345678
20 |
21 |
22 | HTTP/1.1 200 OK
23 | Content-Type: application/javascript
24 | Content-Length: ...
25 |
26 | foo([ { "firstName": "John", "lastName": "Doe" },
27 | { "firstName": "Jane", "lastName": "Doe" } ]);
28 |
29 |
30 |
31 | Or, an alternative response:
32 |
33 |
34 | HTTP/1.1 200 OK
35 | Content-Type: application/javascript
36 | Content-Length: ...
37 |
38 | var foo = [ { "firstName": "John", "lastName": "Doe" }, /* as before */ ];
39 |
40 |
41 |
42 | Now further suppose that you were visiting a page on evil.com that contained
43 | this (we're assuming the first kind of response, where a function call is
44 | returned. A similar exploit exists for the second kind of response, where you
45 | just read the value of the xssi variable that is now set):
46 |
47 |
48 |
49 | <script>
50 | function xssi(obj) {
51 | // code to leak the contact information to evil.com.
52 | }
53 | </script>
54 | <script src="https://example.com/contacts?jsonp=xssi></script>
55 |
56 |
57 |
58 | The contact data would now be processed on evil.com! In order to prevent
59 | this, we have a few options:
60 |
61 |
62 | - Make the returned Javascript unlikely to execute by inserting a prefix that
63 | will break the Javascript parser (like,
)]}'\n).
64 | - Make it so the Javascript is not returned for a GET request, so when
65 | evil.com tries to include it, an empty response is returned.
66 |
67 |
68 | Using the above mitigations, Javascript that you write simply has to
69 | either:
70 |
71 |
72 | - Remove the parser breaking prefix (e.g. by calling
substring()
73 | on the returned data).
74 | - Simply make a POST request to retrieve the data.
75 |
76 |
77 |
78 | As it turns out, our secure framework handles all the server-side pieces for
79 | you! If your WSGI handlers extend from BaseAjaxHandler,
80 | AuthenticatedAjaxHandler, or AdminAjaxHandler,
81 | then GET requests will automatically have that prefix appended.
82 | POST requests don't have any prefix, but for the authenticated
83 | variants, they will require a valid XSRF token in order to process the
84 | request.
85 |
86 | If you're not familiar with XSRF, please click here to
87 | learn how the framework helps defend against that attack. Don't be alarmed
88 | if you're asked to log in - it's necessary for the demonstration.
89 | {% endblock %}
90 |
--------------------------------------------------------------------------------
/templates/xss.tpl:
--------------------------------------------------------------------------------
1 | {% extends "base.tpl" %}
2 | {% block title %}
3 | Cross-Site Scripting
4 | {% endblock %}
5 | {% block content %}
6 | Cross-Site Scripting
7 | Cross-Site Scripting (XSS) occurs when user input is output by a web server
8 | without being properly escaped for the context in which it is
9 | displayed.
10 |
11 | Consider a simple page like this one, which, when given a name, will print
12 | a simple greeting, e.g. "Hello, Jane!"
13 | What happens if a user decides to enter something malicious for their name,
14 | maybe something like:
15 | <script>alert(1);</script>
16 |
17 | If we output that string directly in our page, we would execute the
18 | (possibly malicious) Javascript! Go ahead, try entering that Javascript
19 | snippet:
20 |
21 |
30 |
31 | {% if not autoescape %}
32 | Note that in the default case, no alert box was fired, because the
33 | framework enabled autoescaping by default. Unfortunately,
34 | this autoescaping is not perfect, and actually has some limitations
35 | around context - the Python template systems only perform HTML escaping,
36 | which means converting characters like < to <, > to >
37 | and " to ".
38 | This becomes a problem if you wanted to do something like this in a
39 | template, though:
40 |
41 | <script>
42 | var foo = '{{ '{{' }}user_controlled_string}}';
43 | </script>
44 |
45 | This is problematic because the user could provide this input:
46 |
47 | ' + alert(1) + '
48 |
49 | Note that none of these characters would be HTML escaped, and the resulting
50 | code block would be:
51 |
52 | <script>
53 | var foo = '' + alert(1) + '';
54 | </script>
55 |
56 | The Javascript interpreter will execute the alert expression to build the
57 | string, and we have a cross-site scripting vulnerability. To avoid these,
58 | we recommend avoiding constructs like this in your code. If you must have
59 | this construct, then template systems like Jinja2 and Django often provide
60 | a specific Javascript escaping function, but this is quite error prone.
61 | If you are looking for a truly better way, you should investigate using a
62 | contextually aware autoescaping template system, such as
63 |
64 | Closure Templates.
65 |
66 | XSS is a
67 | complicated topic, and these aren't the only attack vectors - please be
68 | sure you understand the risks inherent in handling user input.
69 | Ready to move on? Click here to learn about Cross-Site
70 | Script Inclusion.
71 | {% endif %}
72 | Output:
73 |
74 | {% if string %}
75 | Hello,
76 | {% if autoescape %}
77 | {% include "autoescape.tpl" %}
78 | {% else %}
79 | {% include "noautoescape.tpl" %}
80 | {% endif %}
81 | {% endif %}
82 | {% if autoescape and string %}
83 | Now try again, but you might want to check the "disable autoescaping" checkbox.
84 | In order to enable this option you need to modify base/handler.py and add "jinja2.ext.autoescape" extension in jinja2 config setting.
85 |
86 | {% endif %}
87 | {% endblock %}
88 |
--------------------------------------------------------------------------------
/third_party/README.md:
--------------------------------------------------------------------------------
1 | Use the `py` and `js` directories to track third party Python/Javascript code
2 | used in building, deploying, or serving the site / application. The contents
3 | will be copied during the build process to `out/` and `out/static/third_party`
4 | respectively.
5 |
6 | Check in a pristine copy
7 | ========================
8 |
9 | The first commit should be the version of the code as it was
10 | downloaded. This allows us to track changes.
11 |
12 | This commit should also contain LICENSE and README.local files.
13 |
14 | Please only add one new library with each commit. If you want to use multiple
15 | third party packages please split them in to one commit per package.
16 |
17 | LICENSE
18 | =======
19 |
20 | To be used, third party code must be licensed.
21 |
22 | The license for the file must be in a file named LICENSE in the root
23 | directory in which the code was imported.
24 |
25 | If it was not distributed like that you need to create a LICENSE file
26 | (perhaps by renaming LICENSE.txt or COPYING to LICENSE). If the license
27 | is only available in comments in the code, or at a particular URL then
28 | extract and copy the text of the license in to LICENSE.
29 |
30 | If you do generate a LICENSE file document it in the "Local Modifications"
31 | section of README.local as follows:
32 |
33 | LICENSE file has been created for compliance purposes. Not included in
34 | the original distribution.
35 |
36 | and include a brief description explaining how you generated the LICENSE file.
37 |
38 | If a given piece of third party code is under multiple licenses then include
39 | all of them in the LICENSE file.
40 |
41 | Please wrap the LICENSE file to 80 characters and replace any
42 | non-ASCII characters with their ASCII equivalents.
43 |
44 | README.local
45 | =============
46 |
47 | This file allows people to quickly understand what this package is for.
48 | The structure is:
49 |
50 | URL: http:// # This should point to the download URL from which you
51 | # obtained this specific version of the package. Examples:
52 | # http://example.org/packagename-0.3.tar.gz
53 | # https://github.com///archive/[.zip
54 | # https://bitbucket.org///get/][.zip
55 | #
56 | https://.googlesource.com//+archive/][.tar.gz
57 | # http://.googlecode.com/archive/.zip (only for
58 | git or hg projects)
59 | # http://..googlecode.com/archive/.zip
60 | # http://.googlecode.com/svn-history//trunk
61 | # https://svn.code.sf.net/p//code/trunk/?p=
62 | (you can also use the tag path if using a tagged release)
63 | Version: XXX # e.g., version string of the package, such as: 0.3
64 | # rNNN for svn revision NNN; tag for git; the entire hash for
65 | git, hg
66 | # YYYY-MM-DD of date downloaded if *no other* version is
67 | available
68 | License: XXX # e.g., GPL v2, GPL v3, LGPL v2.1, Apache 2.0, BSD, MIT, etc.
69 | License File: # should be LICENSE (see the above instructions)
70 |
71 | Description:
72 | # Short description of the package
73 | The Foo framework provides support for geo-location based on the
74 | time of day and position of the Sun.
75 |
76 | Local Modifications:
77 | No modifications.
78 |
79 | The "Local Modifications" section is for detailing whether any changes
80 | have been made to the package (that might, for example, trigger clauses
81 | in the license).
82 |
83 | The URL should be the versioned packaged you downloaded. **Do not provide
84 | an un-versioned URL or a URL to the project page**.
85 |
--------------------------------------------------------------------------------
/src/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Tests for main."""
15 |
16 | import unittest2
17 | import webapp2
18 | import webapp2_extras.routes
19 |
20 | from base import handlers
21 | import main
22 |
23 |
24 | class MainTest(unittest2.TestCase):
25 | """Test cases for main."""
26 |
27 | def _VerifyInheritance(self, routes_list, base_class):
28 | """Checks that the handlers of the given routes inherit from base_class."""
29 | router = webapp2.Router(routes_list)
30 | routes = router.match_routes + router.build_routes.values()
31 | inheritance_errors = ''
32 | for route in routes:
33 | if issubclass(route.__class__, webapp2_extras.routes.MultiRoute):
34 | self._VerifyInheritance(list(route.get_routes()), base_class)
35 | continue
36 |
37 | if issubclass(route.handler, webapp2.RedirectHandler):
38 | continue
39 |
40 | if not issubclass(route.handler, base_class):
41 | inheritance_errors += '* %s does not inherit from %s.\n' % (
42 | route.handler.__name__, base_class.__name__)
43 |
44 | return inheritance_errors
45 |
46 | def testRoutesInheritance(self):
47 | errors = ''
48 | errors += self._VerifyInheritance(main._UNAUTHENTICATED_ROUTES,
49 | handlers.BaseHandler)
50 | errors += self._VerifyInheritance(main._UNAUTHENTICATED_AJAX_ROUTES,
51 | handlers.BaseAjaxHandler)
52 | errors += self._VerifyInheritance(main._USER_ROUTES,
53 | handlers.AuthenticatedHandler)
54 | errors += self._VerifyInheritance(main._AJAX_ROUTES,
55 | handlers.AuthenticatedAjaxHandler)
56 | errors += self._VerifyInheritance(main._ADMIN_ROUTES,
57 | handlers.AdminHandler)
58 | errors += self._VerifyInheritance(main._ADMIN_AJAX_ROUTES,
59 | handlers.AdminAjaxHandler)
60 | errors += self._VerifyInheritance(main._CRON_ROUTES,
61 | handlers.BaseCronHandler)
62 | errors += self._VerifyInheritance(main._TASK_ROUTES,
63 | handlers.BaseTaskHandler)
64 | if errors:
65 | self.fail('Some handlers do not inherit from the correct classes:\n' +
66 | errors)
67 |
68 | def testStrictHandlerMethodRouting(self):
69 | """Checks that handler functions properly limit applicable HTTP methods."""
70 | router = webapp2.Router(main._USER_ROUTES + main._AJAX_ROUTES +
71 | main._ADMIN_ROUTES + main._ADMIN_AJAX_ROUTES)
72 | routes = router.match_routes + router.build_routes.values()
73 | failed_routes = []
74 | while routes:
75 | route = routes.pop()
76 | if issubclass(route.__class__, webapp2_extras.routes.MultiRoute):
77 | routes += list(route.get_routes())
78 | continue
79 |
80 | if issubclass(route.handler, webapp2.RedirectHandler):
81 | continue
82 |
83 | if route.handler_method and not route.methods:
84 | failed_routes.append('%s (%s)' % (route.template,
85 | route.handler.__name__))
86 |
87 | if failed_routes:
88 | self.fail('Some handlers specify a handler_method but are missing a '
89 | 'methods" attribute and may be vulnerable to XSRF via GET '
90 | 'requests:\n * ' + '\n * '.join(failed_routes))
91 |
92 |
93 | if __name__ == '__main__':
94 | unittest2.main()
95 |
--------------------------------------------------------------------------------
/src/examples/example_handlers.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from google.appengine.api import memcache # For XsrfHandler. Remove if unused.
15 | from google.appengine.api import users
16 |
17 | from base import constants
18 | from base import handlers
19 |
20 | from generated import example
21 |
22 | # Example handlers to demonstrate functionality.
23 | # Replace with your own implementations.
24 | class ClosureXssHandler(handlers.BaseHandler):
25 |
26 | def get(self):
27 | self.render(example.xss,
28 | { 's': self.request.get('string', '') })
29 |
30 | class JinjaXssHandler(handlers.BaseHandler):
31 |
32 | def get(self):
33 | # Test for jinja extension
34 | extensions = self.get_jinja2_config()['environment_args']['extensions']
35 | autoescape_ext = 'jinja2.ext.autoescape' in extensions
36 |
37 | if not constants.IS_DEV_APPSERVER:
38 | self.render('debug_only.tpl')
39 | return
40 | autoescape = self.request.get('autoescape') != 'off'
41 | string = self.request.get('string', '')
42 | template = {'string': string,
43 | 'autoescape': autoescape,
44 | 'show_autoescape': bool(string) and autoescape_ext}
45 | # DANGER: Disable CSP and the built-in XSS blocker in modern browsers for
46 | # demonstration purposes. DO NOT DUPLICATE THIS IN PRODUCTION CODE.
47 | self.response.headers['X-XSS-Protection'] = '0'
48 | self.response.headers['content-security-policy'] = ''
49 | self.render('xss.tpl', template)
50 |
51 | def post(self):
52 | self.get()
53 |
54 | class XsrfHandler(handlers.AuthenticatedHandler):
55 |
56 | def _GetCounter(self):
57 | counter = memcache.get('counter')
58 | if not counter:
59 | counter = 0
60 | memcache.set('counter', counter)
61 | return counter
62 |
63 | def get(self):
64 | if not constants.IS_DEV_APPSERVER:
65 | self.render('debug_only.tpl')
66 | return
67 | counter = self._GetCounter()
68 | self.render('xsrf.tpl', {'email': self.current_user.email(),
69 | 'counter': counter})
70 |
71 | def post(self):
72 | if not constants.IS_DEV_APPSERVER:
73 | self.render('debug_only.tpl')
74 | return
75 | counter = self._GetCounter() + 1
76 | memcache.set('counter', counter)
77 | self.render('xsrf.tpl', {'email': self.current_user.email(),
78 | 'counter': counter})
79 |
80 | def DenyAccess(self):
81 | self.redirect(users.create_login_url(self.request.path))
82 |
83 | def XsrfFail(self):
84 | counter = self._GetCounter()
85 | self.render('xsrf.tpl', {'email': self.current_user.email(),
86 | 'counter': counter,
87 | 'xsrf_fail': True})
88 |
89 |
90 | class XssiHandler(handlers.BaseHandler):
91 |
92 | def get(self):
93 | if not constants.IS_DEV_APPSERVER:
94 | self.render('debug_only.tpl')
95 | return
96 | self.render('xssi.tpl')
97 |
98 | def post(self):
99 | self.get()
100 |
101 |
102 | class CspHandler(handlers.BaseHandler):
103 |
104 | def get(self):
105 | # Test for jinja extension
106 | extensions = self.get_jinja2_config()['environment_args']['extensions']
107 | autoescape_ext = 'jinja2.ext.autoescape' in extensions
108 |
109 | if not constants.IS_DEV_APPSERVER:
110 | self.render('debug_only.tpl')
111 | return
112 | self.render('csp.tpl')
113 |
114 | def post(self):
115 | self.get()
116 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Main application entry point."""
15 |
16 | import base.api_fixer
17 |
18 | import webapp2
19 |
20 | import base
21 | import base.constants
22 | import handlers
23 |
24 | # Example handlers
25 | from examples import example_handlers
26 |
27 |
28 | # These should all inherit from base.handlers.BaseHandler
29 | _UNAUTHENTICATED_ROUTES = [('/', handlers.RootHandler),
30 | ('/examples/xss',
31 | example_handlers.ClosureXssHandler),
32 | ('/examples/jinja',
33 | example_handlers.JinjaXssHandler),
34 | ('/examples/csp', example_handlers.CspHandler),
35 | ('/examples/xssi', example_handlers.XssiHandler)]
36 |
37 | # These should all inherit from base.handlers.BaseAjaxHandler
38 | _UNAUTHENTICATED_AJAX_ROUTES = [('/csp', handlers.CspHandler)]
39 |
40 | # These should all inherit from base.handlers.AuthenticatedHandler
41 | _USER_ROUTES = [('/examples/xsrf', example_handlers.XsrfHandler)]
42 |
43 | # These should all inherit from base.handlers.AuthenticatedAjaxHandler
44 | _AJAX_ROUTES = []
45 |
46 | # These should all inherit from base.handlers.AdminHandler
47 | _ADMIN_ROUTES = []
48 |
49 | # These should all inherit from base.handlers.AdminAjaxHandler
50 | _ADMIN_AJAX_ROUTES = []
51 |
52 | # These should all inherit from base.handlers.BaseCronHandler
53 | _CRON_ROUTES = []
54 |
55 | # These should all inherit from base.handlers.BaseTaskHandler
56 | _TASK_ROUTES = []
57 |
58 | # Place global application configuration settings (e.g. settings for
59 | # 'webapp2_extras.sessions') here.
60 | #
61 | # These values will be accessible from handler methods like this:
62 | # self.app.config.get('foo')
63 | #
64 | # Framework level settings:
65 | # template: one of base.constants.CLOSURE (default), base.constants.DJANGO,
66 | # or base.constants.JINJA.
67 | #
68 | # using_angular: True or False (default). When True, an XSRF-TOKEN cookie
69 | # will be set for interception/use by Angular's $http service.
70 | # When False, no header will be set (but an XSRF token will
71 | # still be available under the _xsrf key for Django/Jinja
72 | # templates). If you set this to True, be especially careful
73 | # when mixing Angular and any server side templates:
74 | # https://github.com/angular/angular.js/issues/5601
75 | # See the summary by IgorMinar for details.
76 | #
77 | # framing_policy: one of base.constants.DENY (default),
78 | # base.constants.SAMEORIGIN, or base.constants.PERMIT
79 | #
80 | # hsts_policy: A dictionary with minimally a 'max_age' key, and optionally
81 | # a 'includeSubdomains' boolean member.
82 | # Default: { 'max_age': 2592000, 'includeSubDomains': True }
83 | # implying 30 days of strict HTTPS for all subdomains.
84 | #
85 | # csp_policy: A dictionary with keys that correspond to valid CSP
86 | # directives, as defined in the W3C CSP 3 spec. Each
87 | # key/value pair is transmitted as a distinct
88 | # Content-Security-Policy header.
89 | # Default: {'default-src': '\'self\''}
90 | # which is a very restrictive policy. An optional
91 | # 'reportOnly' boolean key substitutes a
92 | # 'Content-Security-Policy-Report-Only' header
93 | # name in lieu of 'Content-Security-Policy' (the default
94 | # is base.constants.DEBUG).
95 | #
96 | # Note that the default values are also configured in app.yaml for files
97 | # served via the /static/ resources. You may need to change the settings
98 | # there as well.
99 |
100 | _CONFIG = {
101 | 'template': base.constants.CLOSURE,
102 | # Developers are encouraged to build sites that comply with this CSP policy.
103 | # Changing the first two entries (nonce, strict-dynamic) of the script-src
104 | # directive may render XSS protection invalid! For more information take a
105 | # look here https://www.w3.org/TR/CSP3/#strict-dynamic-usage
106 | # With this policy, modern browsers will execute only those scripts whose
107 | # nonce attribute matches the value set in the policy header, as well as
108 | # scripts dynamically added to the page by scripts with the proper nonce.
109 | # Older browsers, which don't support the CSP3 standard, will ignore the
110 | # nonce-* and 'strict-dynamic' keywords and fall back to [script-src
111 | # 'unsafe-inline' https: http:] which will not provide protection against
112 | # XSS vulnerabilities, but will allow the application to function properly.
113 | 'csp_policy': {
114 | # Restrict base tags to same origin, to prevent CSP bypasses.
115 | 'base-uri': '\'self\'',
116 | # Disallow Flash, etc.
117 | 'object-src': '\'none\'',
118 | # Strict CSP with fallbacks for browsers not supporting CSP v3.
119 | 'script-src': base.constants.CSP_NONCE_PLACEHOLDER_FORMAT +
120 | # Propagate trust to dynamically created scripts.
121 | '\'strict-dynamic\' '
122 | # Fallback. Ignored in presence of a nonce
123 | '\'unsafe-inline\' '
124 | # Fallback. Ignored in presence of strict-dynamic.
125 | 'https: http:',
126 | 'report-uri': '/csp',
127 | 'reportOnly': base.constants.DEBUG,
128 | }
129 | }
130 |
131 | #################################
132 | # DO NOT MODIFY BELOW THIS LINE #
133 | #################################
134 |
135 | app = webapp2.WSGIApplication(
136 | routes=(_UNAUTHENTICATED_ROUTES + _UNAUTHENTICATED_AJAX_ROUTES +
137 | _USER_ROUTES + _AJAX_ROUTES + _ADMIN_ROUTES + _ADMIN_AJAX_ROUTES +
138 | _CRON_ROUTES + _TASK_ROUTES),
139 | debug=base.constants.DEBUG,
140 | config=_CONFIG)
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Secure GAE Scaffold for Python 2
2 |
3 | ## Introduction
4 | ----
5 | Please note: this is not an official Google product.
6 |
7 | *This scaffold is for users of App Engine's Python 2.7 runtime. For websites
8 | deployed to the Python 3 runtime, please see [Secure Scaffold for Python 3](https://github.com/google/gae-secure-scaffold-python3).*
9 |
10 | This contains a boilerplate AppEngine application meant to provide a secure
11 | base on which to build additional functionality. Structure:
12 |
13 | * / - top level directory for common files, e.g. app.yaml
14 | * /js - directory for uncompiled Javascript resources.
15 | * /src - directory for all source code
16 | * /static - directory for static content
17 | * /templates - directory for Django/Jinja2 templates your app renders.
18 | * /templates/soy - directory for Closure Templates your application uses.
19 |
20 | Javascript resources for your application can be written using Closure,
21 | and compiled by Google's Closure Compiler (detailed below in the dependencies
22 | section).
23 |
24 | The scaffold provides the following basic security guarantees by default through
25 | a set of base classes found in `src/base/handlers.py`. These handlers:
26 |
27 | 1. Set assorted security headers (Strict-Transport-Security, X-Frame-Options,
28 | X-XSS-Protection, X-Content-Type-Options, Content-Security-Policy) with
29 | strong default values to help avoid attacks like Cross-Site Scripting (XSS)
30 | and Cross-Site Script Inclusion. See `_SetCommonResponseHeaders()` and
31 | `SetAjaxResponseHeaders()`.
32 | 1. Prevent the XSS-prone construction of HTML via string concatenation by
33 | forcing the use of a template system (Django/Jinja2 supported). The
34 | template systems have non-contextual autoescaping enabled by default.
35 | See the `render()`, `render_json()` methods in `BaseHandler` and
36 | `BaseAjaxHandler`. For contextual autoescaping, you should use Closure
37 | Templates in strict mode ().
38 | 1. Test for the presence of headers that guarantee requests to Cron or
39 | Task endpoints are made by the AppEngine serving environment or an
40 | application administrator. See the `dispatch()` method in `BaseCronHandler`
41 | and `BaseTaskHandler`.
42 | 1. Verify XSRF tokens by default on authenticated requests using any verb other
43 | that GET, HEAD, or OPTIONS. See the `_RequestContainsValidXsrfToken()`
44 | method for more information.
45 |
46 | In addition to the protections above, the scaffold monkey patches assorted APIs
47 | that use insecure or dangerous defaults (see `src/base/api_fixer.py`).
48 |
49 | Obviously no framework is perfect, and the flexibility of Python offers many
50 | ways for a motivated developer to circumvent the protections offered. Under
51 | the assumption that developers are not malicious, using the scaffold should
52 | centralize many security mechanisms, provide safe defaults, and structure the
53 | code in a way that facilitates security review.
54 |
55 | Sample implementations can be found in `src/handlers.py`. These demonstrate
56 | basic functionality, and should be removed / replaced by code specific to
57 | your application.
58 |
59 |
60 | ## Prerequisites
61 | ----
62 | These instructions have been tested with the following software:
63 |
64 | * node.js >= 0.8.0
65 | * 0.8.0 is the minimum required to build with [Grunt](http://gruntjs.com/).
66 | * git
67 | * curl
68 |
69 | ## Dependency Setup
70 | ----
71 | From the root of the repository:
72 |
73 | 1. `git submodule init`
74 | 1. `git submodule update`
75 | 1. `cd closure-compiler` - refer to closure-compiler/README.md on how to build
76 | the compiler. Feel free to use this GWT-skipping variant:
77 | `mvn -pl externs/pom.xml,pom-main.xml,pom-main-shaded.xml`
78 | 1. `cd ../closure-templates && mvn && cd ..`
79 | 1. `npm install`
80 | 1. `mkdir $HOME/bin; cd $HOME/bin`
81 | 1. `npm install grunt-cli`
82 | * Alternatively, `sudo npm install -g grunt-cli` will install system-wide
83 | and you may skip the next step.
84 | 1. `export PATH=$HOME/bin/node_modules/grunt-cli/bin:$PATH`
85 | * It is advisable to add this to login profile scripts (.bashrc, etc.).
86 | 1. Visit , and choose
87 | the alternative option to "download the original App Engine SDK for Python."
88 | Choose the "Linux" platform (even if you use OS X). Unzip the file, such
89 | that $HOME/bin/google_appengine/ is populated with the contents of the .zip.
90 |
91 | To install dependencies for unit testing:
92 | 1. `sudo easy_install pip`
93 | 1. `sudo pip install unittest2`
94 |
95 | ## Scaffold Development
96 | ----
97 |
98 | ### Testing
99 | To run unit tests:
100 |
101 | `python run_tests.py ~/bin/google_appengine src`
102 |
103 | ### Local Development
104 | To run the development appserver locally:
105 |
106 | 1. `grunt clean`
107 | 1. `grunt`
108 | 1. `grunt appengine:run:app`
109 |
110 | Note that the development appserver will be running on a snapshot of code
111 | at the time you run it. If you make changes, you can run the various Grunt
112 | tasks in order to propagate them to the local appserver. For instance,
113 | `grunt copy` will refresh the source code (local and third party), static files,
114 | and templates. `grunt closureSoys` and/or `grunt closureBuilder` will rebuild
115 | the templates or your provided Javascript and the updated versions will be
116 | written in the output directory.
117 |
118 | ### Deployment
119 | To deploy to AppEngine:
120 |
121 | 1. `grunt clean`
122 | 1. `grunt --appid=`
123 | 1. `grunt appengine:update:app --appid=`
124 |
125 | Specifying `--appid=` will override any value set in `config.json`. You may
126 | modify the `config.json` file to avoid having to pass this parameter on
127 | every invocation.
128 |
129 | ## Notes
130 | ----
131 | Files in `js/` are compiled by the Closure Compiler (if available) and placed in
132 | `out/static/app.js`. Included in this compilation pass is the the output of
133 | the `closureSoys:js` task (intermediate artifacts: out/generated/js/\*.js).
134 |
135 | Closure Templates that you provide are also compiled using the Python backend,
136 | and are available using the constants.CLOSURE template strategy (the default).
137 | The generated source code is stored in out/generated/\*.py. To use them,
138 | pass the callable template as the first argument to render(), and a dictionary
139 | containing the template values as the second argument, e.g.:
140 |
141 | from generated import helloworld
142 |
143 | [...]
144 |
145 | self.render(helloworld.helloWorld, { 'name': 'first last' })
146 |
147 | The `/static` and `/template` directories are replicated in `out/`, and the
148 | files in `src/` are rebased into `out/` (so `src/base/foo.py` becomes
149 | `out/base/foo.py`).
150 |
--------------------------------------------------------------------------------
/src/base/handlers_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Tests for base.handlers."""
15 |
16 | import exceptions
17 | import unittest2
18 | import webapp2
19 |
20 | import handlers
21 | import xsrf
22 |
23 | from google.appengine.ext import testbed
24 |
25 |
26 | class DummyHandler(handlers.AuthenticatedHandler):
27 | """Convenience class to verify successful requests."""
28 |
29 | def get(self):
30 | self._RawWrite('get_succeeded')
31 |
32 | def post(self):
33 | self._RawWrite('post_succeeded')
34 |
35 | def DenyAccess(self):
36 | self._RawWrite('access_denied')
37 |
38 | def XsrfFail(self):
39 | self._RawWrite('xsrf_fail')
40 |
41 |
42 | class DummyAjaxHandler(handlers.BaseAjaxHandler):
43 | """Convenience class to verify successful requests."""
44 |
45 | def get(self):
46 | pass
47 |
48 | def post(self):
49 | pass
50 |
51 |
52 | class DummyCronHandler(handlers.BaseCronHandler):
53 | """Convenience class to verify successful requests."""
54 |
55 | def get(self):
56 | self._RawWrite('get_succeeded')
57 |
58 |
59 | class DummyTaskHandler(handlers.BaseTaskHandler):
60 | """Convenience class to verify successful requests."""
61 |
62 | def get(self):
63 | self._RawWrite('get_succeeded')
64 |
65 |
66 | class HandlersTest(unittest2.TestCase):
67 | """Test cases for base.handlers."""
68 |
69 | def setUp(self):
70 | self.testbed = testbed.Testbed()
71 | self.testbed.activate()
72 | self.testbed.init_datastore_v3_stub()
73 | self.testbed.init_memcache_stub()
74 | self.app = webapp2.WSGIApplication([('/', DummyHandler),
75 | ('/ajax', DummyAjaxHandler),
76 | ('/cron', DummyCronHandler),
77 | ('/task', DummyTaskHandler)])
78 |
79 | def _FakeLogin(self):
80 | """Sets up the environment to have a fake user logged in."""
81 | self.testbed.setup_env(
82 | USER_EMAIL='user@example.com',
83 | USER_ID='123',
84 | overwrite=True)
85 |
86 | def testHandlerCannotOverrideFinalMethods(self):
87 |
88 | try:
89 |
90 | class _(handlers.BaseHandler):
91 |
92 | def dispatch(self):
93 | pass
94 |
95 | self.fail('should not be able to override dispatch')
96 | except handlers.SecurityError, e:
97 | self.assertTrue(e.message.find('override restricted') != -1)
98 |
99 | def testAuthenticatedHandlerRequiresUser(self):
100 |
101 | self.assertEqual('access_denied', self.app.get_response('/').body)
102 | self.assertEqual('access_denied', self.app.get_response('/',
103 | method='POST').body)
104 | self._FakeLogin()
105 | self.assertEqual('get_succeeded', self.app.get_response('/').body)
106 |
107 | def testXsrfProtectionFailsWithInvalidToken(self):
108 | self._FakeLogin()
109 | self.assertEqual('xsrf_fail', self.app.get_response('/',
110 | method='POST',
111 | POST={}).body)
112 |
113 | def testXsrfProtectionSucceedsWithValidToken(self):
114 | self._FakeLogin()
115 |
116 | key = handlers._GetXsrfKey()
117 | token = xsrf.GenerateToken(key, 'user@example.com')
118 | self.assertEqual('post_succeeded',
119 | self.app.get_response('/',
120 | method='POST',
121 | POST={'xsrf': token}).body)
122 |
123 | def testResponseHasStrictCSP(self):
124 | """Checks that the CSP in the response is set and strict.
125 | More information: https://www.w3.org/TR/CSP3/#strict-dynamic-usage
126 | """
127 | fakeNonce = 'rand0m123'
128 | strictBaseUri = ['\'self\'']
129 | strictScriptSrc = ['\'strict-dynamic\'', '\'nonce-%s\'' % fakeNonce]
130 | strictObjectSrc = ['\'none\'']
131 |
132 | handlers._GetCspNonce = lambda : fakeNonce
133 |
134 | headers = self.app.get_response('/', method='GET').headers
135 | csp_header = headers.get('Content-Security-Policy')
136 | self.assertIsNotNone(csp_header)
137 | csp = {x.split()[0]: x.split()[1:] for x in csp_header.split(';')}
138 |
139 | # Check that csp contains a nonce and the stict-dynamic keyword.
140 | self.assertTrue(set(strictScriptSrc) <= set(csp.get('script-src')))
141 | self.assertListEqual(strictBaseUri, csp.get('base-uri'))
142 | self.assertListEqual(strictObjectSrc, csp.get('object-src'))
143 |
144 | def testAjaxGetResponsesIncludeXssiPrefix(self):
145 | self.assertEqual(handlers._XSSI_PREFIX, self.app.get_response('/ajax').body)
146 |
147 | def testAjaxPostResponsesLackXssiPrefix(self):
148 | self.assertEqual('', self.app.get_response('/ajax', method='POST').body)
149 |
150 | def testCronFailsWithoutXAppEngineCron(self):
151 | try:
152 | self.app.get_response('/cron', method='GET')
153 | self.fail('Cron succeeded without X-AppEngine-Cron: true header')
154 | except exceptions.AssertionError, e:
155 | # webapp2 wraps the raised SecurityError during dispatch with an
156 | # exceptions.AssertionError.
157 | self.assertTrue(e.message.find('X-AppEngine-Cron') != -1)
158 |
159 | def testCronSucceedsWithXAppEngineCron(self):
160 | headers = [('X-AppEngine-Cron', 'true')]
161 | self.assertEqual('get_succeeded',
162 | self.app.get_response('/cron',
163 | headers=headers).body)
164 |
165 | def testTaskFailsWithoutXAppEngineQueueName(self):
166 | try:
167 | self.app.get_response('/task', method='GET')
168 | self.fail('Task succeeded without X-AppEngine-QueueName header')
169 | except exceptions.AssertionError, e:
170 | # webapp2 wraps the raised SecurityError during dispatch with an
171 | # exceptions.AssertionError.
172 | self.assertTrue(e.message.find('X-AppEngine-QueueName') != -1)
173 |
174 | def testTaskSucceedsWithXAppEngineQueueName(self):
175 | headers = [('X-AppEngine-QueueName', 'default')]
176 | self.assertEqual('get_succeeded',
177 | self.app.get_response('/task',
178 | headers=headers).body)
179 |
180 | if __name__ == '__main__':
181 | unittest2.main()
182 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | // The target directory for the final build product.
4 | var targetDirectory = 'out';
5 |
6 | grunt.initConfig({
7 | appengine: {
8 | app: {
9 | root: targetDirectory,
10 | manageScript: [process.env.HOME,
11 | 'bin', 'google_appengine', 'appcfg.py'].join('/'),
12 | runFlags: {
13 | port: 8080
14 | },
15 | runScript: [process.env.HOME,
16 | 'bin', 'google_appengine', 'dev_appserver.py'].join('/')
17 | }
18 | },
19 |
20 | build: grunt.file.readJSON('config.json'),
21 |
22 | clean: [targetDirectory],
23 |
24 | closureBuilder: {
25 | options: {
26 | closureLibraryPath: 'closure-library',
27 | compile: true,
28 | compilerFile: ['closure-compiler', 'target',
29 | 'closure-compiler-v20160517.jar'].join('/'),
30 | compilerOpts: {
31 | compilation_level: grunt.option('dev') ?
32 | 'SIMPLE_OPTIMIZATIONS' : 'ADVANCED_OPTIMIZATIONS'
33 | },
34 | namespaces: 'app',
35 | },
36 | js: {
37 | src: ['closure-library', 'js',
38 | [targetDirectory, 'generated', 'js'].join('/')],
39 | dest: [targetDirectory, 'static', 'app.js'].join('/'),
40 | }
41 | },
42 |
43 | closureSoys: {
44 | js: {
45 | src: ['templates', 'soy', '**', '*.soy'].join('/'),
46 | soyToJsJarPath: ['closure-templates', 'target',
47 | 'soy-2016-08-25-SoyToJsSrcCompiler.jar'].join('/'),
48 | outputPathFormat: [targetDirectory, 'generated', 'js',
49 | 'app.soy.js'].join('/'),
50 | options: {
51 | allowExternalCalls: false,
52 | shouldGenerateJsdoc: true,
53 | shouldProvideRequireSoyNamespaces: true
54 | }
55 | },
56 | py: {
57 | src: ['templates', 'soy', '**', '*.soy'].join('/'),
58 | soyToJsJarPath: ['closure-templates', 'target',
59 | 'soy-2016-08-25-SoyToPySrcCompiler.jar'].join('/'),
60 | outputPathFormat: [targetDirectory, 'generated',
61 | '{INPUT_FILE_NAME_NO_EXT}.py'].join('/'),
62 | options: {
63 | runtimePath: 'soy',
64 | }
65 | }
66 | },
67 |
68 | copy: {
69 | source: {
70 | cwd: 'src/',
71 | dest: [targetDirectory, ''].join('/'),
72 | expand: true,
73 | src: '**'
74 | },
75 | soyutils_js: {
76 | cwd: ['closure-templates', 'javascript'].join('/'),
77 | dest: [targetDirectory, 'generated', 'js'].join('/'),
78 | expand: true,
79 | src: 'soyutils_usegoog.js'
80 | },
81 | soyutils_py: {
82 | cwd: ['closure-templates', 'python'].join('/'),
83 | dest: [targetDirectory, 'soy', ''].join('/'),
84 | expand: true,
85 | src: '*.py'
86 | },
87 |
88 | static: {
89 | cwd: 'static',
90 | dest: [targetDirectory, 'static', ''].join('/'),
91 | expand: true,
92 | src: '**'
93 | },
94 | templates: {
95 | cwd: 'templates',
96 | dest: [targetDirectory, 'templates', ''].join('/'),
97 | expand: true,
98 | src: '**'
99 | },
100 | third_party_js: {
101 | cwd: ['third_party', 'js'].join('/'),
102 | dest: [targetDirectory, 'static', 'third_party', ''].join('/'),
103 | expand: true,
104 | src: '**'
105 | },
106 | third_party_py: {
107 | cwd: ['third_party', 'py'].join('/'),
108 | dest: [targetDirectory, ''].join('/'),
109 | expand: true,
110 | src: '**'
111 | }
112 | },
113 | });
114 |
115 | grunt.loadNpmTasks('grunt-appengine');
116 | grunt.loadNpmTasks('grunt-contrib-clean');
117 | grunt.loadNpmTasks('grunt-contrib-copy');
118 | grunt.loadNpmTasks('grunt-closure-soy');
119 | grunt.loadNpmTasks('grunt-closure-tools');
120 |
121 | grunt.registerTask('init_py', 'Generates __init__.py', function(dir) {
122 | grunt.file.write([targetDirectory, dir, '__init__.py'].join('/'),
123 | '');
124 | });
125 | grunt.registerTask('nop', 'no-op', function() {});
126 |
127 | grunt.registerTask('yaml', 'Generates app.yaml',
128 | function() {
129 | var appid = grunt.option('appid') ||
130 | grunt.config.get('build.appid', false);
131 |
132 | if (typeof(appid) !== 'string' || appid.length == 0) {
133 | grunt.fatal('no appid');
134 | }
135 |
136 | var uncommitedChanges = false;
137 | var done = this.async();
138 |
139 | var logCallback = function(error, result, code) {
140 | if (code != 0) {
141 | grunt.log.writeln('git log error: ' + result);
142 | done(false);
143 | }
144 | var hash = String(result).split(' ')[0].substr(0, 16);
145 | var versionString = hash + (uncommitedChanges ? '-dev' : '');
146 | var yamlData = grunt.file.read('app.yaml.base');
147 | yamlData = yamlData.replace('__APPLICATION__', appid);
148 | yamlData = yamlData.replace('__VERSION__', versionString);
149 | grunt.log.writeln('Generating yaml for application: ' + appid);
150 | grunt.file.write([targetDirectory, 'app.yaml'].join('/'), yamlData);
151 | done();
152 | };
153 |
154 | var statusCallback = function(error, result, code) {
155 | if (code != 0) {
156 | grunt.log.writeln('git status error: ' + result);
157 | done(false);
158 | }
159 | var pattern = /nothing to commit, working (directory|tree) clean/i;
160 | if (!pattern.test(String(result))) {
161 | uncommitedChanges = true;
162 | }
163 | grunt.util.spawn(
164 | {cmd: 'git', args: ['log', '--format=oneline', '-n', '1']},
165 | logCallback);
166 | };
167 |
168 | grunt.util.spawn({cmd: 'git', args: ['status']}, statusCallback);
169 | });
170 |
171 | grunt.registerTask('default',
172 | ['copy:source', 'copy:static', 'copy:templates',
173 | 'copy:third_party_js', 'copy:third_party_py',
174 | grunt.config.get('build.use_closure_templates') ?
175 | 'copy:soyutils_js' : 'nop',
176 | grunt.config.get('build.use_closure_templates') ?
177 | 'closureSoys:js' : 'nop',
178 | grunt.config.get('build.use_closure') ? 'closureBuilder' : 'nop',
179 | grunt.config.get('build.use_closure_py_templates') ?
180 | 'copy:soyutils_py' : 'nop',
181 | grunt.config.get('build.use_closure_py_templates') ?
182 | 'init_py:soy' : 'nop',
183 | grunt.config.get('build.use_closure_py_templates') ?
184 | 'init_py:generated' : 'nop',
185 | grunt.config.get('build.use_closure_py_templates') ?
186 | 'closureSoys:py' : 'nop',
187 | 'yaml']);
188 | };
189 |
190 |
--------------------------------------------------------------------------------
/src/base/api_fixer.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """Fixes up various popular APIs to ensure they use secure defaults."""
15 |
16 | import __builtin__
17 | import constants
18 | import cPickle
19 | import functools
20 | import io
21 | import json
22 | import logging
23 | import pickle
24 | import yaml
25 |
26 | from google.appengine.api import urlfetch
27 | from webapp2_extras import sessions
28 |
29 |
30 | class ApiSecurityException(Exception):
31 | """Error when attempting to call an unsafe API."""
32 | pass
33 |
34 |
35 | def FindArgumentIndex(function, argument):
36 | args = function.func_code.co_varnames[:function.func_code.co_argcount]
37 | return args.index(argument)
38 |
39 |
40 | def GetDefaultArgument(function, argument):
41 | argument_index = FindArgumentIndex(function, argument)
42 | num_positional_args = (function.func_code.co_argcount -
43 | len(function.func_defaults))
44 | default_position = argument_index - num_positional_args
45 | if default_position < 0:
46 | return None
47 | return function.func_defaults[default_position]
48 |
49 |
50 | def ReplaceDefaultArgument(function, argument, replacement):
51 | argument_index = FindArgumentIndex(function, argument)
52 | num_positional_args = (function.func_code.co_argcount -
53 | len(function.func_defaults))
54 | default_position = argument_index - num_positional_args
55 | if default_position < 0:
56 | raise ApiSecurityException('Attempt to modify positional default value')
57 | new_defaults = list(function.func_defaults)
58 | new_defaults[default_position] = replacement
59 | function.func_defaults = tuple(new_defaults)
60 |
61 |
62 | # JSON.
63 | # Does not escape HTML metacharacters by default.
64 | _JSON_CHARACTER_REPLACEMENT_MAPPING = [
65 | ('<', '\\u%04x' % ord('<')),
66 | ('>', '\\u%04x' % ord('>')),
67 | ('&', '\\u%04x' % ord('&')),
68 | ]
69 |
70 |
71 | class _JsonEncoderForHtml(json.JSONEncoder):
72 |
73 | def encode(self, o):
74 | chunks = self.iterencode(o, _one_shot=True)
75 | if not isinstance(chunks, (list, tuple)):
76 | chunks = list(chunks)
77 | return ''.join(chunks)
78 |
79 | def iterencode(self, o, _one_shot=False):
80 | chunks = super(_JsonEncoderForHtml, self).iterencode(o, _one_shot)
81 | for chunk in chunks:
82 | for (character, replacement) in _JSON_CHARACTER_REPLACEMENT_MAPPING:
83 | chunk = chunk.replace(character, replacement)
84 | yield chunk
85 |
86 |
87 | ReplaceDefaultArgument(json.dump, 'cls', _JsonEncoderForHtml)
88 | ReplaceDefaultArgument(json.dumps, 'cls', _JsonEncoderForHtml)
89 |
90 |
91 | # Pickle. See http://www.cs.jhu.edu/~s/musings/pickle.html for more info.
92 | # Map of safe module name => (module, [list of safe names])
93 | _PICKLE_CLASS_SAFE_NAMES = { '__builtin__': (__builtin__,
94 | ['basestring',
95 | 'bool',
96 | 'buffer',
97 | 'bytearray',
98 | 'bytes',
99 | 'complex',
100 | 'dict',
101 | 'enumerate',
102 | 'float',
103 | 'frozenset',
104 | 'int',
105 | 'list',
106 | 'long',
107 | 'reversed',
108 | 'set',
109 | 'slice',
110 | 'str',
111 | 'tuple',
112 | 'unicode',
113 | 'xrange']),
114 | }
115 |
116 | # See https://docs.python.org/3/library/pickle.html#restricting-globals.
117 | class RestrictedUnpickler(pickle.Unpickler):
118 |
119 | def find_class(self, module_name, name):
120 | (module, safe_names) = _PICKLE_CLASS_SAFE_NAMES.get(module_name, (None, []))
121 | if name in safe_names:
122 | return getattr(module, name)
123 | raise ApiSecurityException('%s.%s forbidden in unpickling' % (module, name))
124 |
125 | def _SafePickleLoad(f):
126 | return RestrictedUnpickler(f).load()
127 |
128 | def _SafePickleLoads(string):
129 | return RestrictedUnpickler(io.BytesIO(string)).load()
130 |
131 | pickle.load = _SafePickleLoad
132 | pickle.loads = _SafePickleLoads
133 | cPickle.load = _SafePickleLoad
134 | cPickle.loads = _SafePickleLoads
135 |
136 |
137 | # YAML. The Python tag scheme allows arbitrary code execution:
138 | # yaml.load('!!python/object/apply:os.system ["ls"]')
139 | ReplaceDefaultArgument(yaml.compose, 'Loader', yaml.loader.SafeLoader)
140 | ReplaceDefaultArgument(yaml.compose_all, 'Loader', yaml.loader.SafeLoader)
141 | ReplaceDefaultArgument(yaml.load, 'Loader', yaml.loader.SafeLoader)
142 | ReplaceDefaultArgument(yaml.load_all, 'Loader', yaml.loader.SafeLoader)
143 | ReplaceDefaultArgument(yaml.parse, 'Loader', yaml.loader.SafeLoader)
144 | ReplaceDefaultArgument(yaml.scan, 'Loader', yaml.loader.SafeLoader)
145 |
146 |
147 | # AppEngine urlfetch.
148 | # Does not validate certificates by default.
149 | ReplaceDefaultArgument(urlfetch.fetch, 'validate_certificate', True)
150 | ReplaceDefaultArgument(urlfetch.make_fetch_call, 'validate_certificate', True)
151 |
152 |
153 | def _HttpUrlLoggingWrapper(func):
154 | """Decorates func, logging when 'url' params do not start with https://."""
155 | @functools.wraps(func)
156 | def _CheckAndLog(*args, **kwargs):
157 | try:
158 | arg_index = FindArgumentIndex(func, 'url')
159 | except ValueError:
160 | return func(*args, **kwargs)
161 |
162 | if arg_index < len(args):
163 | arg_value = args[arg_index]
164 | elif 'url' in kwargs:
165 | arg_value = kwargs['url']
166 | elif 'url' not in kwargs:
167 | arg_value = GetDefaultArgument(func, 'url')
168 |
169 | if arg_value and not arg_value.startswith('https://'):
170 | logging.warn('SECURITY : fetching non-HTTPS url %s' % (arg_value))
171 | return func(*args, **kwargs)
172 | return _CheckAndLog
173 |
174 | urlfetch.fetch = _HttpUrlLoggingWrapper(urlfetch.fetch)
175 | urlfetch.make_fetch_call = _HttpUrlLoggingWrapper(urlfetch.make_fetch_call)
176 |
177 | # webapp2_extras session does not set HttpOnly/Secure by default.
178 | sessions.default_config['cookie_args']['secure'] = (not
179 | constants.IS_DEV_APPSERVER)
180 | sessions.default_config['cookie_args']['httponly'] = True
181 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/base/handlers.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Google Inc. All rights reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | """A collection of secure base handlers for webapp2-based applications."""
15 |
16 | import abc
17 | import base64
18 | import django.conf
19 | import django.template
20 | import django.template.loader
21 | import functools
22 |
23 | import json
24 | import webapp2
25 | from webapp2_extras import jinja2
26 |
27 | import api_fixer
28 | import constants
29 | import models
30 | import os
31 | import xsrf
32 |
33 | from google.appengine.api import memcache
34 | from google.appengine.api import users
35 |
36 |
37 | # Django initialization.
38 | django.conf.settings.configure(DEBUG=constants.DEBUG,
39 | TEMPLATE_DEBUG=constants.DEBUG,
40 | TEMPLATE_DIRS=[constants.TEMPLATE_DIR])
41 |
42 |
43 | # Assorted decorators that can be used inside a webapp2.RequestHandler object
44 | # to assert certain preconditions before entering any method.
45 | def requires_auth(f):
46 | """A decorator that requires a currently logged in user."""
47 | @functools.wraps(f)
48 | def wrapper(self, *args, **kwargs):
49 | if not users.get_current_user():
50 | self.DenyAccess()
51 | else:
52 | return f(self, *args, **kwargs)
53 | return wrapper
54 |
55 |
56 | def requires_admin(f):
57 | """A decorator that requires a currently logged in administrator."""
58 | @functools.wraps(f)
59 | def wrapper(self, *args, **kwargs):
60 | if not users.is_current_user_admin():
61 | self.DenyAccess()
62 | else:
63 | return f(self, *args, **kwargs)
64 | return wrapper
65 |
66 |
67 | def xsrf_protected(f):
68 | """Decorator to validate XSRF tokens for any verb but GET, HEAD, OPTIONS."""
69 | @functools.wraps(f)
70 | def wrapper(self, *args, **kwargs):
71 | non_xsrf_protected_verbs = ['options', 'head', 'get']
72 | if (self.request.method.lower() in non_xsrf_protected_verbs or
73 | self._RequestContainsValidXsrfToken()):
74 | return f(self, *args, **kwargs)
75 | else:
76 | self.XsrfFail()
77 | return wrapper
78 |
79 |
80 | # Utility functions.
81 | def _GetXsrfKey():
82 | """Returns the current key for generating and verifying XSRF tokens."""
83 | client = memcache.Client()
84 | xsrf_key = client.get('xsrf_key')
85 | if not xsrf_key:
86 | config = models.GetApplicationConfiguration()
87 | xsrf_key = config.xsrf_key
88 | client.set('xsrf_key', xsrf_key)
89 | return xsrf_key
90 |
91 |
92 | def _GetCspNonce():
93 | """Returns a random CSP nonce."""
94 | nonce_length = constants.NONCE_LENGTH
95 | return base64.b64encode(os.urandom(nonce_length * 2))[:nonce_length]
96 |
97 |
98 | # Classes with a __metaclass__ of _HandlerMeta may not contain any methods
99 | # with these names. This is checked when the class is instantiated.
100 | _RESTRICTED_FUNCTION_LIST = [
101 | 'dispatch',
102 | '_RequestContainsValidXsrfToken',
103 | ]
104 |
105 | # Classes with these names (and _HandlerMeta as a metaclass) can contain
106 | # functions in the _RESTRICTED_FUNCTION_LIST. Note that there is no
107 | # package/module specified, so it is possible to bypass this check through
108 | # clever (or malicious) naming.
109 | _RESTRICTED_FUNCTION_TRUSTED_CLASSES = [
110 | 'BaseHandler',
111 | 'BaseAjaxHandler',
112 | 'BaseCronHandler',
113 | 'BaseTaskHandler',
114 | 'AuthenticatedHandler',
115 | 'AuthenticatedAjaxHandler',
116 | 'AdminHandler',
117 | 'AdminAjaxHandler',
118 | ]
119 |
120 | # This prefix is returned on GET requests to any Ajax-like handler.
121 | # It is used to prevent JSON-like responses that may contain non-public
122 | # information from being included in malicious domains, e.g. evil.com
123 | # inserting a tag like: .
124 | # evil.com cannot strip this prefix before parsing the result, unlike
125 | # same-origin requests. See https://google-gruyere.appspot.com/part3
126 | # for more information. It is not necessary for POST requests because
127 | # there is no way to force the browser to make a cross-domain POST request
128 | # and interpret the response as Javascript without use of other mechanisms
129 | # like Cross-Origin-Resource-Sharing, which is disabled by default.
130 | _XSSI_PREFIX = ')]}\',\n'
131 |
132 |
133 | class SecurityError(Exception):
134 | pass
135 |
136 |
137 | class _HandlerMeta(abc.ABCMeta):
138 | """Metaclass for our secure base handlers.
139 |
140 | When a class with this metaclass is defined, the fields
141 | are checked to ensure that certain methods we would like to approximate as
142 | 'final' are not declared in subclasses. This is because we provide a
143 | default implementation which enforces various security related functionality.
144 |
145 | Class names that can bypass this check are listed in
146 | _RESTRICTED_FUNCTION_TRUSTED_CLASSES. Restricted methods are listed in
147 | _RESTRICTED_FUNCTION_LIST.
148 | """
149 |
150 | def __new__(mcs, name, bases, dct):
151 | if name not in _RESTRICTED_FUNCTION_TRUSTED_CLASSES:
152 | for func in _RESTRICTED_FUNCTION_LIST:
153 | if func in dct:
154 | raise SecurityError('%s attempts to override restricted method %s' %
155 | (name, func))
156 | return super(_HandlerMeta, mcs).__new__(mcs, name, bases, dct)
157 |
158 |
159 | class BaseHandler(webapp2.RequestHandler):
160 | """Base handler for servicing unauthenticated user requests."""
161 |
162 | __metaclass__ = _HandlerMeta
163 |
164 | def __init__(self, request, response):
165 | self.initialize(request, response)
166 | api_fixer.ReplaceDefaultArgument(response.set_cookie.im_func, 'secure',
167 | not constants.IS_DEV_APPSERVER)
168 | api_fixer.ReplaceDefaultArgument(response.set_cookie.im_func, 'httponly',
169 | True)
170 | if self.current_user:
171 | self._xsrf_token = xsrf.GenerateToken(_GetXsrfKey(),
172 | self.current_user.email())
173 | if self.app.config.get('using_angular', constants.DEFAULT_ANGULAR):
174 | # AngularJS requires a JS readable XSRF-TOKEN cookie and will pass this
175 | # back in AJAX requests.
176 | self.response.set_cookie('XSRF-TOKEN', self._xsrf_token, httponly=False)
177 | else:
178 | self._xsrf_token = None
179 |
180 | self.csp_nonce = _GetCspNonce()
181 |
182 | self._RawWrite = self.response.out.write
183 | self.response.out.write = self._ReplacementWrite
184 |
185 | # All content should be rendered through a template system to reduce the
186 | # risk/likelihood of XSS issues. Access to the original function
187 | # self.response.out.write is available via self._RawWrite for exceptional
188 | # circumstances.
189 | def _ReplacementWrite(*args, **kwargs):
190 | raise SecurityError('All response content must originate via render() or'
191 | 'render_json()')
192 |
193 | def _SetCommonResponseHeaders(self):
194 | """Sets various headers with security implications."""
195 | frame_policy = self.app.config.get('framing_policy', constants.DENY)
196 | frame_header_value = constants.X_FRAME_OPTIONS_VALUES.get(frame_policy, '')
197 | if frame_header_value:
198 | self.response.headers['X-Frame-Options'] = frame_header_value
199 |
200 | hsts_policy = self.app.config.get('hsts_policy',
201 | constants.DEFAULT_HSTS_POLICY)
202 | if self.request.scheme.lower() == 'https' and hsts_policy:
203 | include_subdomains = bool(hsts_policy.get('includeSubdomains', False))
204 | subdomain_string = '; includeSubdomains' if include_subdomains else ''
205 | hsts_value = 'max-age=%d%s' % (int(hsts_policy.get('max_age')),
206 | subdomain_string)
207 | self.response.headers['Strict-Transport-Security'] = hsts_value
208 |
209 | self.response.headers['X-XSS-Protection'] = '1; mode=block'
210 | self.response.headers['X-Content-Type-Options'] = 'nosniff'
211 |
212 | csp_policy = self.app.config.get('csp_policy', constants.DEFAULT_CSP_POLICY)
213 | report_only = False
214 | if 'reportOnly' in csp_policy:
215 | report_only = csp_policy.get('reportOnly')
216 | csp_policy = csp_policy.copy()
217 | del csp_policy['reportOnly']
218 | header_name = ('Content-Security-Policy%s' %
219 | ('-Report-Only' if report_only else ''))
220 | directives = []
221 | for (k, v) in csp_policy.iteritems():
222 | directives.append('%s %s' % (k, v))
223 | csp = '; '.join(directives)
224 |
225 | # Set random nonce per response
226 | csp = csp % {'nonce_value': self.csp_nonce}
227 |
228 | self.response.headers.add(header_name, csp)
229 |
230 | @webapp2.cached_property
231 | def current_user(self):
232 | return users.get_current_user()
233 |
234 | def dispatch(self):
235 | self._SetCommonResponseHeaders()
236 | super(BaseHandler, self).dispatch()
237 |
238 |
239 | @classmethod
240 | def get_jinja2_config(cls):
241 | """
242 | Builds Jinja2 config based on constants.
243 |
244 | Note: this is used in the factory below, but an alternative way of setting
245 | up Jinja2 would be to use the WSGIApplication config to set this and not
246 | use the factory below. This has the advantage of having different settings
247 | for different applications and not set here at the handler level.
248 | """
249 | extensions = ['jinja2.ext.with_']
250 | return {
251 | 'environment_args': {
252 | 'autoescape': True,
253 | 'extensions': extensions,
254 | 'auto_reload': constants.DEBUG,
255 | },
256 | 'template_path': constants.TEMPLATE_DIR
257 | }
258 |
259 | @staticmethod
260 | def j2_factory(app):
261 | """
262 | The factory function passed to get_jinja2.
263 | Args:
264 | app: the WSGIApplication
265 | """
266 | return jinja2.Jinja2(app, BaseHandler.get_jinja2_config())
267 |
268 | @webapp2.cached_property
269 | def jinja2(self):
270 | """
271 | Get the cached Jinja2 instance from the app registry, if none exists
272 | the factory function is used to create one.
273 | """
274 | return jinja2.get_jinja2(self.j2_factory, app=self.app)
275 |
276 | def render_to_string(self, template, template_values=None):
277 | """Renders template_name with template_values and returns as a string."""
278 | if not template_values:
279 | template_values = {}
280 |
281 | template_values['_xsrf'] = self._xsrf_token
282 | template_values['_csp_nonce'] = self.csp_nonce
283 | template_strategy = self.app.config.get('template', constants.CLOSURE)
284 |
285 | if template_strategy == constants.DJANGO:
286 | t = django.template.loader.get_template(template)
287 | template_values = django.template.Context(template_values)
288 | return t.render(template_values)
289 | elif template_strategy == constants.JINJA2:
290 | return self.jinja2.render_template(template, **template_values)
291 | else:
292 | ijdata = { 'csp_nonce': self.csp_nonce }
293 | return template(template_values, ijdata)
294 |
295 | def render(self, template, template_values=None):
296 | """Renders template with template_values and writes to the response."""
297 | template_strategy = self.app.config.get('template', constants.CLOSURE)
298 | self._RawWrite(self.render_to_string(template, template_values))
299 |
300 |
301 | class BaseCronHandler(BaseHandler):
302 | """Base handler for servicing Cron requests.
303 |
304 | This handler enforces that inbound requests contain the X-AppEngine-Cron
305 | header, which AppEngine guarantees is only present on actual invocations
306 | according to the cron schedule, or crafted requests by an administrator
307 | of the application (the header is filtered out from normal user requests).
308 | """
309 |
310 | __metaclass__ = _HandlerMeta
311 |
312 | def dispatch(self):
313 | header = self.request.headers.get('X-AppEngine-Cron', 'false')
314 | if header != 'true':
315 | raise SecurityError('attempt to access cron handler without '
316 | 'X-AppEngine-Cron header')
317 | super(BaseCronHandler, self).dispatch()
318 |
319 |
320 | class BaseTaskHandler(BaseHandler):
321 | """Base handler for servicing task requests.
322 |
323 | This handler enforces that inbound requests contain the X-AppEngine-QueueName
324 | header, which AppEngine guarantees is only present on requests from the
325 | Task Queue API, or crafted requests by an administrator of the application
326 | (the header is filtered out from normal user requests).
327 | """
328 |
329 | __metaclass__ = _HandlerMeta
330 |
331 | def dispatch(self):
332 | header = self.request.headers.get('X-AppEngine-QueueName', None)
333 | if not header:
334 | raise SecurityError('attempt to access task handler without '
335 | 'X-AppEngine-QueueName header')
336 | super(BaseTaskHandler, self).dispatch()
337 |
338 |
339 | class BaseAjaxHandler(BaseHandler):
340 | """Base handler for servicing unauthenticated AJAX requests.
341 |
342 | Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests
343 | using other HTTP verbs will not include such a prefix.
344 | """
345 |
346 | __metaclass__ = _HandlerMeta
347 |
348 | def _SetAjaxResponseHeaders(self):
349 | self.response.headers['Content-Disposition'] = 'attachment; filename=json'
350 | self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
351 |
352 | def dispatch(self):
353 | self._SetAjaxResponseHeaders()
354 | if self.request.method.lower() == 'get':
355 | self._RawWrite(_XSSI_PREFIX)
356 | super(BaseAjaxHandler, self).dispatch()
357 |
358 | def render(self, *args, **kwargs):
359 | raise SecurityError('AJAX handlers must use render_json()')
360 |
361 | def render_json(self, obj):
362 | self._RawWrite(json.dumps(obj))
363 |
364 |
365 | class AuthenticatedHandler(BaseHandler):
366 | """Base handler for servicing authenticated user requests.
367 |
368 | Implementations should provide an implementation of DenyAccess()
369 | and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens.
370 |
371 | POST requests will be rejected unless the request contains a
372 | parameter named 'xsrf' which is a valid XSRF token for the
373 | currently authenticated user.
374 | """
375 |
376 | __metaclass__ = _HandlerMeta
377 |
378 | @requires_auth
379 | @xsrf_protected
380 | def dispatch(self):
381 | super(AuthenticatedHandler, self).dispatch()
382 |
383 | def _RequestContainsValidXsrfToken(self):
384 | token = self.request.get('xsrf') or self.request.headers.get('X-XSRF-TOKEN')
385 | # By default, Angular's $http service will add quotes around the
386 | # X-XSRF-TOKEN.
387 | if (token and
388 | self.app.config.get('using_angular', constants.DEFAULT_ANGULAR) and
389 | token[0] == '"' and token[-1] == '"'):
390 | token = token[1:-1]
391 |
392 | if xsrf.ValidateToken(_GetXsrfKey(), self.current_user.email(),
393 | token):
394 | return True
395 | return False
396 |
397 | @abc.abstractmethod
398 | def DenyAccess(self):
399 | pass
400 |
401 | @abc.abstractmethod
402 | def XsrfFail(self):
403 | pass
404 |
405 |
406 | class AuthenticatedAjaxHandler(BaseAjaxHandler):
407 | """Base handler for servicing AJAX requests.
408 |
409 | Implementations should provide an implementation of DenyAccess()
410 | and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens.
411 |
412 | POST requests will be rejected unless the request contains a
413 | parameter named 'xsrf', OR an HTTP header named 'X-XSRF-Token'
414 | which is a valid XSRF token for the currently authenticated user.
415 |
416 | Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests
417 | using other HTTP verbs will not include such a prefix.
418 | """
419 |
420 | __metaclass__ = _HandlerMeta
421 |
422 | @requires_auth
423 | @xsrf_protected
424 | def dispatch(self):
425 | super(AuthenticatedAjaxHandler, self).dispatch()
426 |
427 | def _RequestContainsValidXsrfToken(self):
428 | token = self.request.get('xsrf') or self.request.headers.get('X-XSRF-Token')
429 | # By default, Angular's $http service will add quotes around the
430 | # X-XSRF-TOKEN.
431 | if (token and
432 | self.app.config.get('using_angular', constants.DEFAULT_ANGULAR) and
433 | token[0] == '"' and token[-1] == '"'):
434 | token = token[1:-1]
435 |
436 | if xsrf.ValidateToken(_GetXsrfKey(), self.current_user.email(),
437 | token):
438 | return True
439 | return False
440 |
441 | @abc.abstractmethod
442 | def DenyAccess(self):
443 | pass
444 |
445 | @abc.abstractmethod
446 | def XsrfFail(self):
447 | pass
448 |
449 |
450 | class AdminHandler(AuthenticatedHandler):
451 | """Base handler for servicing administrator requests.
452 |
453 | Implementations should provide an implementation of DenyAccess()
454 | and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens.
455 |
456 | Requests will be rejected if the currently logged in user is
457 | not an administrator.
458 |
459 | POST requests will be rejected unless the request contains a
460 | parameter named 'xsrf' which is a valid XSRF token for the
461 | currently authenticated user.
462 | """
463 |
464 | __metaclass__ = _HandlerMeta
465 |
466 | @requires_admin
467 | def dispatch(self):
468 | super(AdminHandler, self).dispatch()
469 |
470 |
471 | class AdminAjaxHandler(AuthenticatedAjaxHandler):
472 | """Base handler for servicing AJAX administrator requests.
473 |
474 | Implementations should provide an implementation of DenyAccess()
475 | and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens.
476 |
477 | Requests will be rejected if the currently logged in user is
478 | not an administrator.
479 |
480 | POST requests will be rejected unless the request contains a
481 | parameter named 'xsrf', OR an HTTP header named 'X-XSRF-Token'
482 | which is a valid XSRF token for the currently authenticated user.
483 |
484 | Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests
485 | using other HTTP verbs will not include such a prefix.
486 | """
487 |
488 | __metaclass__ = _HandlerMeta
489 |
490 | @requires_admin
491 | def dispatch(self):
492 | super(AdminAjaxHandler, self).dispatch()
493 |
--------------------------------------------------------------------------------
]