commit ffc30d4eb425090484310249ea19602cfa8829c5 Author: Robert Rothenberg Date: Fri Jul 3 11:29:11 2026 +0100 Add support for OAuth2 state to fix CVE-2026-12740 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Robert Rothenberg diff --git a/Makefile.PL b/Makefile.PL index 295013e..544591a 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -10,7 +10,8 @@ readme_from 'lib/Plack/Middleware/OAuth.pm'; readme_markdown_from 'lib/Plack/Middleware/OAuth.pm'; requires - 'Plack::Middleware::Session' => 0, + 'Plack::Middleware::Session' => 0.35, + 'Crypt::SysRandom' => 0, 'File::Spec' => 0, 'DateTime' => 0, 'Digest::MD5' => 0, diff --git a/lib/Plack/Middleware/OAuth/Handler/AccessTokenV2.pm b/lib/Plack/Middleware/OAuth/Handler/AccessTokenV2.pm index 1aedbc9..0b6c4c7 100644 --- a/lib/Plack/Middleware/OAuth/Handler/AccessTokenV2.pm +++ b/lib/Plack/Middleware/OAuth/Handler/AccessTokenV2.pm @@ -1,9 +1,11 @@ +use utf8; package Plack::Middleware::OAuth::Handler::AccessTokenV2; use parent qw(Plack::Middleware::OAuth::Handler); use URI; use URI::Query; use LWP::UserAgent; use Plack::Middleware::OAuth::AccessToken; +use Plack::Session; use Try::Tiny; use JSON::Any; use warnings; @@ -46,9 +48,9 @@ sub get_access_token { my %params; # we are pretty sure, the response is json format - if( $content_type =~ m{json} - || $content_type =~ m{javascript} - || $response_content =~ m{^\{.*?\}\s*$}s ) + if( $content_type =~ m{json} + || $content_type =~ m{javascript} + || $response_content =~ m{^\{.*?\}\s*$}s ) { try { %params = %{ JSON::Any->new->decode( $response_content ) }; # should be hashref. @@ -56,7 +58,7 @@ sub get_access_token { # XXX: show exception page for this. die "Can not decode json: " . $_; }; - } + } else { try { my $qq = URI::Query->new( $ua_response->content ); @@ -79,6 +81,21 @@ sub get_access_token { sub run { my $self = $_[0]; + my $provider = $self->provider; + my $env = $self->env; + + # CSRF defense (RFC 6749 §10.12): the callback must carry the + # same `state` we issued in RequestTokenV2 and stored in the + # session. Mismatch / absence => login-CSRF attempt; refuse. + my $session = Plack::Session->new($env); + my $expected_state = $session->get("oauth.${provider}.state"); + my $req_state = $self->param('state'); + if ( !defined($expected_state) || !defined($req_state) + || $expected_state ne $req_state ) { + return $self->render('OAuth state mismatch (login-CSRF defense).'); + } + $session->remove("oauth.${provider}.state"); + my $code = $self->param('code'); # https://graph.facebook.com/oauth/access_token? @@ -97,8 +114,6 @@ sub run { } # register oauth args to session - my $env = $self->env; - my $provider = $self->provider; $token->register_session($env); diff --git a/lib/Plack/Middleware/OAuth/Handler/RequestTokenV2.pm b/lib/Plack/Middleware/OAuth/Handler/RequestTokenV2.pm index cdc5263..3907936 100644 --- a/lib/Plack/Middleware/OAuth/Handler/RequestTokenV2.pm +++ b/lib/Plack/Middleware/OAuth/Handler/RequestTokenV2.pm @@ -2,6 +2,8 @@ package Plack::Middleware::OAuth::Handler::RequestTokenV2; use warnings; use strict; use parent qw(Plack::Middleware::OAuth::Handler); +use Crypt::SysRandom qw(random_bytes); +use Plack::Session; sub default_callback { my $self = shift; @@ -17,6 +19,16 @@ sub default_callback { sub run { my $self = shift; my $config = $self->config; + my $provider = $self->provider; + my $env = $self->env; + + # CSRF defense (RFC 6749 §10.12): generate a CSPRNG-backed state + # token, store it in the session keyed by provider name, send it + # to the upstream so the callback can verify the response was + # initiated by THIS browser session. + my $state = unpack("H*", random_bytes(16)); + my $session = Plack::Session->new($env); + $session->set("oauth.${provider}.state", $state); # "https://www.facebook.com/dialog/oauth?client_id=YOUR_APP_ID&redirect_uri=YOUR_URL"; my $uri = URI->new( $config->{authorize_url} ); @@ -25,6 +37,7 @@ sub run { redirect_uri => $config->{redirect_uri} || $self->default_callback, response_type => $config->{response_type} || 'code', scope => $config->{scope}, + state => $state, ); $uri->query_form( %query ); return $self->redirect( $uri );