AJAX / SOSP / CORS / CSRF

(Ou 4 siglas, 16 letras e uma dor de cabeça)

Agenda

Mostrar como aplicações web ricas se comportam quando o conteúdo está vindo de uma fonte que não a fonte original.

A Idéia

Gerar uma view com conteúdo que será inserido em outro site; o conteúdo deverá abrir um modal para pedir mais dados para o usuário.

Passo 1: O Conteúdo

Carregado por AJAX, com jQuery. Fácil.


$(function() {
    $("#div-especial").load("http://outrosite.com/conteudo.html");
});
					

Problema 1: SOSP (Same Origin Security Policy)

Same Origin Security Policy é assegurado pelo browser, barrando requisições vindas de lugares que não são o lugar original.

"Lugar original" = URL requisitada tem mesmo esquema (http vs https), mesmo domínio (subdomínios não contam), mesma porta (ou implício 80) da URL que está fazendo a requisição.

Página em http://outrosite.com/conteudo.html pede:

  • http://outrosite.com/dados.json ⇒ OK
  • http://outrosite.com/dir/dados.json ⇒ OK
  • http://usuário:senha@outrosite.com/dir/dados.json ⇒ OK
  • http://outrosite.com:8080/dados.json ⇒ NOT!
  • https://outrosite.com/dados.json ⇒ NOT!
  • http://api.outrosite.com/dados.json ⇒ NOT!
  • http://outrosite.com:80/dados.json ⇒ talvez...

Apenas lembrando: isso é forçado pelo browser, não pelo serviço.

Outras aplicações podem passar por cima do Same Origin se quiserem.

Restrições de subdomínios podem ser relaxadas se scripts forem carregados de subdomínios diferentes:

Se a página em http://outrosite.com/ carregar um script de http://api.outrosite.com, a restrição de subdomínios pode ser removida para funções do script.

Se não tiver como relaxar as restrições, utiliza-se CORS.

Problema 2: CORS (Cross-Origin Resource Sharing)

CORS é implementado no servidor e diz se o serviço pode ou não utilizar aquele recurso.

Browser rodando em http://outrosite.com requisita serviço em http://api.outrosite.com/.

Para isso, envia o header Origin.


Origin: http://outrosite.com
						

Servidor olha o header, verifica se a URL tem permissão para acessar os recursos, é retornado o header Access-Control-Allow-Origin.


Access-Control-Allow-Origin: http://outrosite.com
						

De novo, isso é controlado pelo browser; um browser que não siga corretamente o controle de acesso ou um aplicativo qualquer poderiam passar por cima dessa restrição e continuar lendo o conteúdo.

Django-CORS-Headers

App para configurar CORS sozinho.


CORS_ORIGIN_WHITELIST = (
   'outrosite.com',
)
						

ou


CORS_ORIGIN_REGEX_WHITELIST = (
    '^(https?://)?(\w+\.)?outrosite\.com$', 
)
						

Agora o site externo consegue carregar o conteúdo das views.

Problema 2.5: URLs reversas

Para resolver URLs, usamos a tag {% url vew_id %} para retornar a URL da view.

O problema é que é retornada a URL absoluta para a view, sem considerar o domínio da view.


<a href='/view/'>
						

em http://outrosite.com vira http://outrosite.com/view/.

Mas se a view estiver num domínio diferente, o mesmo não é considerado.

  • Se http://outrosite.com carregar o conteúdo de http://api.outrosite.com
  • ... e http://api.outrosite.com tiver uma view com URL absoluta /view/
  • ... o browser irá resolver como http://outrosite.com/view/
  • ... quando deveria ser http://api.outrosite.com/view/.

Solução: considerar que SITE_URL (do settings.py) está correto e usar nas URLs.


<a href='{{ settings.SITE_URL }}{% url "view_id" %}'>
						

Problema 3: CSRF

Resumo:

  • "csrftoken" é gerado na sessão do usuário.
  • Campo "csrftoken" é adicionado no form.
  • Quando o form retorna no POST, é verificado se é o mesmo indicado na sessão do usuário.

Problema: Informações do token é passada num cookie.


Set-Cookie: "csrftoken=t5HBi8EbkPk340nnpkdb8qxQsy2n8LwY;
            expires=Tue, 04-Aug-2015 16:40:38 GMT; Max-Age=31449600; Path=/"
						

Cookies não são processados durante requisições AJAX.

Solução correta:

Ao receber uma requisição AJAX, processar os headers também, verificar a existência de "Set-Cookie", verificar se "csrftoken" está na lista, guardar o valor e usar nas requisições seguintes.

Solução utilizada:


from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def minha_view(request):
    ....
						

Problema 3.5: CSRF em Class Based Views


class MinhaView(View):
    @csrf_exempt
    def post(self, request):
        ....
						

... não funciona.

É preciso aplicar decorators no dispatch da view:


    @csrf_exempt
    def dispatch(self, *args, **kwargs):
        u"""Altera o dispatch para dispensar CSRF (por cauxa do AJAX)."""
        return super(MinhaView, self).dispatch(*args, **kwargs)
						

Dica: Evite ter objetos cortantes próximos quando estiver lidando com AJAX/SOSP/CORS/CSRF.