Completed
Push — master ( d01ad3...6b97e6 )
by Paweł
21s queued 10s
created

AnalyticsEventConsumer::handleArticlePageViews()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 3
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Superdesk Web Publisher Core Bundle.
7
 *
8
 * Copyright 2017 Sourcefabric z.ú. and contributors.
9
 *
10
 * For the full copyright and license information, please see the
11
 * AUTHORS and LICENSE files distributed with this source code.
12
 *
13
 * @copyright 2017 Sourcefabric z.ú
14
 * @license http://www.superdesk.org/license
15
 */
16
17
namespace SWP\Bundle\CoreBundle\Consumer;
18
19
use Doctrine\Common\Persistence\ObjectManager;
20
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
21
use PhpAmqpLib\Message\AMQPMessage;
22
use SWP\Bundle\AnalyticsBundle\Model\ArticleEventInterface;
23
use SWP\Bundle\AnalyticsBundle\Services\ArticleStatisticsServiceInterface;
24
use SWP\Bundle\CoreBundle\Model\ArticleStatistics;
25
use SWP\Component\MultiTenancy\Context\TenantContextInterface;
26
use SWP\Component\MultiTenancy\Exception\TenantNotFoundException;
27
use SWP\Component\MultiTenancy\Resolver\TenantResolver;
28
use Symfony\Component\HttpFoundation\Request;
29
30
final class AnalyticsEventConsumer implements ConsumerInterface
31
{
32
    private $articleStatisticsService;
33
34
    private $tenantResolver;
35
36
    private $tenantContext;
37
38
    private $articleStatisticsObjectManager;
39
40
    public function __construct(
41
        ArticleStatisticsServiceInterface $articleStatisticsService,
42
        TenantResolver $tenantResolver,
43
        TenantContextInterface $tenantContext,
44
        ObjectManager $articleStatisticsObjectManager
45
    ) {
46
        $this->articleStatisticsService = $articleStatisticsService;
47
        $this->tenantResolver = $tenantResolver;
48
        $this->tenantContext = $tenantContext;
49
        $this->articleStatisticsObjectManager = $articleStatisticsObjectManager;
50
    }
51
52
    public function execute(AMQPMessage $message)
53
    {
54
        /** @var Request $request */
55
        $request = unserialize($message->getBody());
56
        if (!$request instanceof Request) {
57
            return ConsumerInterface::MSG_REJECT;
58
        }
59
60
        try {
61
            $this->setTenant($request);
62
        } catch (TenantNotFoundException $e) {
63
            echo $e->getMessage()."\n";
64
65
            return ConsumerInterface::MSG_REJECT;
66
        }
67
        echo 'Set tenant: '.$this->tenantContext->getTenant()->getCode()."\n";
68
69
        if ($request->query->has('articleId')) {
70
            $this->handleArticlePageViews($request);
71
72
            echo 'Pageview for article '.$request->query->get('articleId')." was processed \n";
0 ignored issues
show
Security Cross-Site Scripting introduced by
'Pageview for article ' ...') . ' was processed ' can contain request data and is used in output context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: $this->parameters['HTTP_AUTHORIZATION'] seems to return tainted data, and $authorizationHeader is assigned in ServerBag.php on line 62
  1. $this->parameters['HTTP_AUTHORIZATION'] seems to return tainted data, and $authorizationHeader is assigned
    in vendor/ServerBag.php on line 62
  2. ParameterBag::$parameters is assigned
    in vendor/ServerBag.php on line 77
  3. Tainted property ParameterBag::$parameters is read
    in vendor/ParameterBag.php on line 84
  4. ParameterBag::get() returns tainted data
    in src/SWP/Bundle/CoreBundle/Consumer/AnalyticsEventConsumer.php on line 72
  2. Path: Read from $_POST, and $_POST is passed to Request::createRequestFromFactory() in Request.php on line 281
  1. Read from $_POST, and $_POST is passed to Request::createRequestFromFactory()
    in vendor/Request.php on line 281
  2. $request is passed to Request::__construct()
    in vendor/Request.php on line 1953
  3. $request is passed to Request::initialize()
    in vendor/Request.php on line 235
  4. $request is passed to ParameterBag::__construct()
    in vendor/Request.php on line 253
  5. ParameterBag::$parameters is assigned
    in vendor/ParameterBag.php on line 31
  6. Tainted property ParameterBag::$parameters is read
    in vendor/ParameterBag.php on line 84
  7. ParameterBag::get() returns tainted data
    in src/SWP/Bundle/CoreBundle/Consumer/AnalyticsEventConsumer.php on line 72

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
73
        }
74
75
        return ConsumerInterface::MSG_ACK;
76
    }
77
78
    private function handleArticlePageViews(Request $request): void
79
    {
80
        $articleId = $request->query->get('articleId', null);
81
        if (null !== $articleId && 0 !== (int) $articleId) {
82
            $articleStatistics = $this->articleStatisticsService->addArticleEvent((int) $articleId, ArticleEventInterface::ACTION_PAGEVIEW, [
83
                'pageViewSource' => $this->getPageViewSource($request),
84
            ]);
85
86
            $query = $this->articleStatisticsObjectManager->createQuery('UPDATE '.ArticleStatistics::class.' s SET s.pageViewsNumber = s.pageViewsNumber + 1 WHERE s.id = :id');
87
            $query->setParameter('id', $articleStatistics->getId());
88
            $query->execute();
89
        }
90
    }
91
92
    private function getPageViewSource(Request $request): string
93
    {
94
        $pageViewReferer = $request->query->get('ref', null);
95
        if (null !== $pageViewReferer) {
96
            $refererHost = $this->getFragmentFromUrl($pageViewReferer, 'host');
97
            if ($refererHost && $this->isHostMatchingTenant($refererHost)) {
98
                return ArticleEventInterface::PAGEVIEW_SOURCE_INTERNAL;
99
            }
100
        }
101
102
        return ArticleEventInterface::PAGEVIEW_SOURCE_EXTERNAL;
103
    }
104
105
    private function getFragmentFromUrl(string $url, string $fragment): ?string
106
    {
107
        $fragments = \parse_url($url);
108
        if (!\array_key_exists($fragment, $fragments)) {
109
            return null;
110
        }
111
112
        return $fragments[$fragment];
113
    }
114
115
    private function isHostMatchingTenant(string $host): bool
116
    {
117
        $tenant = $this->tenantContext->getTenant();
118
        $tenantHost = $tenant->getDomainName();
119
        if (null !== ($subdomain = $tenant->getSubdomain())) {
120
            $tenantHost = $subdomain.'.'.$tenantHost;
121
        }
122
123
        return $host === $tenantHost;
124
    }
125
126
    private function setTenant(Request $request): void
127
    {
128
        $this->tenantContext->setTenant(
129
            $this->tenantResolver->resolve(
130
                $request->server->get('HTTP_REFERER',
131
                    $request->query->get('host',
132
                        $request->getHost()
133
                    )
134
                )
135
            )
136
        );
137
    }
138
}
139