├── src ├── __init__.py ├── base │ ├── __init__.py │ ├── models.py │ ├── models_test.py │ ├── xsrf.py │ ├── api_fixer_test.py │ ├── xsrf_test.py │ ├── constants.py │ ├── handlers_test.py │ ├── api_fixer.py │ └── handlers.py ├── examples │ ├── __init__.py │ └── example_handlers.py ├── handlers.py ├── main_test.py └── main.py ├── third_party ├── py │ └── __init__.py ├── js │ └── .gitignore └── README.md ├── templates ├── autoescape.tpl ├── soy │ ├── README │ ├── helloworld.soy │ └── example.soy ├── noautoescape.tpl ├── debug_only.tpl ├── base.tpl ├── csp.tpl ├── xsrf.tpl ├── xssi.tpl └── xss.tpl ├── .gitignore ├── config.json ├── package.json ├── .gitmodules ├── AUTHORS ├── js └── app.js ├── CONTRIBUTORS ├── static └── index.html ├── run_tests.py ├── app.yaml.base ├── CONTRIBUTING.md ├── README.md ├── Gruntfile.js └── LICENSE /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /third_party/py/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/autoescape.tpl: -------------------------------------------------------------------------------- 1 | {{string}} 2 | -------------------------------------------------------------------------------- /third_party/js/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | node_modules/ 3 | .DS_Store 4 | *.pyc 5 | *.swp 6 | *~ 7 | -------------------------------------------------------------------------------- /templates/soy/README: -------------------------------------------------------------------------------- 1 | This directory should contain files with an extension of .soy that will be 2 | compiled by the Google Closure Template compiler. 3 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "appid": "scaffold", 4 | "use_closure": true, 5 | "use_closure_templates": true, 6 | "use_closure_py_templates": true 7 | } 8 | 9 | -------------------------------------------------------------------------------- /templates/noautoescape.tpl: -------------------------------------------------------------------------------- 1 | {# WARNING: DISABLING AUTOESCAPE IS DANGEROUS. DO NOT DISABLE IN PRODUCTION. #} 2 | {% autoescape false %} 3 | {{string}} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/debug_only.tpl: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl" %} 2 | {% block title %} 3 | Unavailable 4 | {% endblock %} 5 | {% block content %} 6 |

Unavailable

7 |

Sorry, but this functionality is only available in debug mode. Try running 8 | this example using dev_appserver.

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gae-scaffold", 3 | "version": "0.0.1", 4 | "devDependencies": { 5 | "grunt": "~0.4.5", 6 | "grunt-appengine": "~0.1.6", 7 | "grunt-contrib-clean": "~1.0.0", 8 | "grunt-contrib-copy": "~1.0.0", 9 | "grunt-closure-soy": "~0.2.1", 10 | "grunt-closure-tools": "~0.9.9" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/base.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% block title %}{% endblock %} - My Webpage 6 | {% endblock %} 7 | 8 | 9 |
10 | {% block content %} 11 | {% endblock %} 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "closure-library"] 2 | path = closure-library 3 | url = https://github.com/google/closure-library/ 4 | [submodule "closure-templates"] 5 | path = closure-templates 6 | url = https://github.com/google/closure-templates.git 7 | [submodule "closure-compiler"] 8 | path = closure-compiler 9 | url = https://github.com/google/closure-compiler.git 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of gae-secure-scaffold-python authors for 2 | # copyright purposes. 3 | # This file is distinct from the CONTRIBUTORS files. 4 | # See the latter for an explanation. 5 | 6 | # Names should be added to this file as: 7 | # Name or Organization 8 | # The email address is not required for organizations. 9 | 10 | Google Inc. 11 | Psycle Interactive Ltd 12 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Entry point for GAE Scaffold application. 3 | */ 4 | 5 | goog.provide('app'); 6 | goog.require('goog.events'); 7 | 8 | /** 9 | * Entry point for GAE scaffold application. 10 | */ 11 | app.main = function() { 12 | console.log('app.main() entry point'); 13 | } 14 | 15 | goog.exportSymbol('app.main', app.main); 16 | 17 | goog.events.listen(window, goog.events.EventType.LOAD, app.main); 18 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | # The AUTHORS file lists the copyright holders; this file 3 | # lists people. For example, Google employees are listed here 4 | # but not in AUTHORS, because Google holds the copyright. 5 | # 6 | # https://developers.google.com/open-source/cla/individual 7 | # https://developers.google.com/open-source/cla/corporate 8 | # 9 | # Names should be added to this file as: 10 | # Name 11 | Justin Grayston justin.grayston@psycle.com 12 | -------------------------------------------------------------------------------- /templates/soy/helloworld.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 app.templates autoescape="strict"} 11 | 12 | /** 13 | * A "hello, world" template. 14 | * 15 | * @param? name The name of the person to say hello to. 16 | */ 17 | {template .helloWorld} 18 |

Hello, {if $name}{$name}{else}world{/if}!

19 | {/template} 20 | 21 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Secure GAE Scaffold Application 4 | 5 | 6 |

Hello!

7 |

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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/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 | import json 15 | import logging 16 | 17 | from base import handlers 18 | 19 | # Minimal set of handlers to let you display main page with examples 20 | class RootHandler(handlers.BaseHandler): 21 | 22 | def get(self): 23 | self.redirect('/static/index.html') 24 | 25 | class CspHandler(handlers.BaseAjaxHandler): 26 | 27 | def post(self): 28 | try: 29 | report = json.loads(self.request.body) 30 | logging.warn('CSP Violation: %s' % (json.dumps(report['csp-report']))) 31 | self.render_json({}) 32 | except: 33 | self.render_json({'error': 'invalid CSP report'}) 34 | -------------------------------------------------------------------------------- /src/base/models.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 | """Framework wide datastore models.""" 15 | 16 | from google.appengine.ext import ndb 17 | 18 | import os 19 | 20 | @ndb.transactional 21 | def GetApplicationConfiguration(): 22 | """Returns the application configuration, creating it if necessary.""" 23 | key = ndb.Key(Config, 'config') 24 | entity = key.get() 25 | if not entity: 26 | entity = Config(key=key) 27 | entity.xsrf_key = os.urandom(16) 28 | entity.put() 29 | return entity 30 | 31 | 32 | class Config(ndb.Model): 33 | """A simple key-value store for application configuration settings.""" 34 | 35 | xsrf_key = ndb.BlobProperty() 36 | -------------------------------------------------------------------------------- /src/base/models_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.models.""" 15 | 16 | import unittest2 17 | 18 | import models 19 | 20 | from google.appengine.ext import testbed 21 | 22 | 23 | class ModelsTest(unittest2.TestCase): 24 | """Test cases for base.models.""" 25 | 26 | def setUp(self): 27 | self.testbed = testbed.Testbed() 28 | self.testbed.activate() 29 | self.testbed.init_datastore_v3_stub() 30 | 31 | def testConfigurationAutomaticallyGenerated(self): 32 | config = models.GetApplicationConfiguration() 33 | self.assertIsNotNone(config) 34 | self.assertIsNotNone(config.xsrf_key) 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest2.main() 39 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import optparse 4 | import sys 5 | import unittest2 6 | 7 | USAGE = """%prog SDK_PATH TEST_PATH 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 |

57 | 58 |
59 | 60 |
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 |
58 | 60 |
61 | 62 |
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 |
22 | 23 |
24 | {% if show_autoescape %} 25 | 26 |
27 | {% endif %} 28 | 29 |
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 &lt;, > to &gt; 37 | and " to &quot;.

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 | --------------------------------------------------------------------------------