Issues (162)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/controllers/BaseRestController.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Ntb\RestAPI;
4
5
use Director;
6
use Exception;
7
use HTMLText;
8
use Member;
9
use SS_HTTPRequest;
10
use SS_HTTPResponse;
11
use SS_Log;
12
13
/**
14
 * Base class for the rest resource controllers.
15
 * @author Christian Blank <[email protected]>
16
 */
17
abstract class BaseRestController extends \Controller {
18
19
    private static $allowed_actions = array (
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
20
        'options' => true,
21
        'head' => true
22
    );
23
24
    /**
25
     * Configuration option.
26
     * If set to true, only https connections will be processed.
27
     * @var bool
28
     */
29
    private static $https_only = true;
30
31
    /**
32
     *
33
     */
34
    public function init() {
35
        parent::init();
36
        // check for https
37
        if($this->config()->https_only && !Director::is_https()) {
38
            $response = $this->getResponse();
39
            $response->setStatusCode('403', 'http request not allowed');
40
            $response->setBody("Request over HTTP is not allowed. Please switch to https.");
41
            $response->output();
42
            exit;
43
        }
44
        // check for CORS options request
45
        if ($this->request->httpMethod() === 'OPTIONS' ) {
46
            // create direct response without requesting any controller
47
            $response = $this->getResponse();
48
            // set CORS header from config
49
            $response = $this->addCORSHeaders($response);
50
            $response->output();
51
            exit;
52
        }
53
    }
54
55
    /**
56
     * @param SS_HTTPRequest $request
57
     * @return null
0 ignored issues
show
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
58
     * @throws RestUserException
59
     */
60
    public function head(SS_HTTPRequest $request) {
61
        if(method_exists($this, 'get')) {
62
            $result = $this->get($request);
0 ignored issues
show
Documentation Bug introduced by
The method get does not exist on object<Ntb\RestAPI\BaseRestController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
63
            if($result instanceof SS_HTTPResponse) {
64
                $result->setBody(null);
65
                return $result;
66
            }
67
            return null;
68
        }
69
        throw new RestUserException("Endpoint doesn't have a GET implementation", 404);
70
    }
71
72
    /**
73
     * handleAction implementation for rest controllers. This handles the requested action differently then the standard
74
     * implementation.
75
     *
76
     * @param SS_HTTPRequest $request
77
     * @param string $action
78
     * @return HTMLText|SS_HTTPResponse
79
     */
80
    protected function handleAction($request, $action) {
81
        foreach($request->latestParams() as $k => $v) {
82
            if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
83
        }
84
        // set the action to the request method / for developing we could use an additional parameter to choose another method
85
        $action = $this->getMethodName($request);
86
        $this->action = $action;
87
        $this->requestParams = $request->requestVars();
0 ignored issues
show
Documentation Bug introduced by
It seems like $request->requestVars() can be null. However, the property $requestParams is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
88
        $className = $this->class;
89
        // create serializer
90
        $serializer = SerializerFactory::create_from_request($request);
91
        $response = $this->getResponse();
92
        // perform action
93
        try {
94
            if(!$this->hasAction($action)) {
95
                // method couldn't found on controller
96
                throw new RestUserException("Action '$action' isn't available on class $className.", 404);
97
            }
98
            if(!$this->checkAccessAction($action)) {
99
                throw new RestUserException("Action '$action' isn't allowed on class $className.", 404, 401);
100
            }
101
            $actionResult = null;
102
            if(method_exists($this, 'beforeCallActionHandler')) {
103
                // call before action hook
104
                $actionResult = $this->beforeCallActionHandler($request, $action);
0 ignored issues
show
Documentation Bug introduced by
The method beforeCallActionHandler does not exist on object<Ntb\RestAPI\BaseRestController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
105
            }
106
            // if action hook contains data it will be used as result, otherwise the action handler will be called
107
            if(!$actionResult) {
108
                // perform action
109
                $actionResult = $this->$action($request);
110
            }
111
            $body = $actionResult;
112
        } catch(RestUserException $ex) {
113
            // a user exception was caught
114
            $response->setStatusCode($ex->getHttpStatusCode());
115
            $body = [
116
                'message' => $ex->getMessage(),
117
                'code' => $ex->getCode()
118
            ];
119
            // log all data
120
            SS_Log::log(
121
                json_encode(array_merge($body, ['file' => $ex->getFile(), 'line' => $ex->getLine()])),
122
                SS_Log::INFO);
123
        } catch(RestSystemException $ex) {
124
            // a system exception was caught
125
            $response->addHeader('Content-Type', $serializer->contentType());
126
            $response->setStatusCode($ex->getHttpStatusCode());
127
            $body = [
128
                'message' => $ex->getMessage(),
129
                'code' => $ex->getCode()
130
            ];
131 View Code Duplication
            if(Director::isDev()) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
132
                $body = array_merge($body, [
133
                    'file' => $ex->getFile(),
134
                    'line' => $ex->getLine(),
135
                    'trace' => $ex->getTrace()
136
                ]);
137
            }
138
            // log all data
139
            SS_Log::log(
140
                json_encode(array_merge($body, ['file' => $ex->getFile(), 'line' => $ex->getLine()])),
141
                SS_Log::WARN);
142
        } catch(Exception $ex) {
143
            // an unexpected exception was caught
144
            $response->addHeader('Content-Type', $serializer->contentType());
145
            $response->setStatusCode("500");
146
            $body = [
147
                'message' => $ex->getMessage(),
148
                'code' => $ex->getCode()
149
            ];
150 View Code Duplication
            if(Director::isDev()) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
151
                $body = array_merge($body, [
152
                    'file' => $ex->getFile(),
153
                    'line' => $ex->getLine(),
154
                    'trace' => $ex->getTrace()
155
                ]);
156
            }
157
            // log all data and the trace to get a better understanding of the exception
158
            SS_Log::log(
159
                json_encode(array_merge(
160
                    $body, ['file' => $ex->getFile(), 'line' => $ex->getLine(),'trace' => $ex->getTrace()])),
161
                SS_Log::ERR);
162
        }
163
        // serialize content and set body of response
164
        $response->addHeader('Content-Type', $serializer->contentType());
165
        // TODO: body could be an exception; check it before the response is generated
166
        $response->setBody($serializer->serialize($body));
167
        // set CORS header from config
168
        $response = $this->addCORSHeaders($response);
169
        return $response;
170
    }
171
172
    /**
173
     * Returns the http method for this request. If the current environment is a development env, the method can be
174
     * changed with a `method` variable.
175
     *
176
     * @param \SS_HTTPRequest $request the current request
177
     * @return string the used http method as string
178
     */
179
    private function getMethodName($request) {
180
        $method = '';
181
        if(Director::isDev() && ($varMethod = $request->getVar('method'))) {
182
            if(in_array(strtoupper($varMethod), ['GET','POST','PUT','DELETE','HEAD', 'PATCH'])) {
183
                $method = $varMethod;
184
            }
185
        } else {
186
            $method = $request->httpMethod();
187
        }
188
        return strtolower($method);
189
    }
190
191
    /**
192
     * Check, if the request is authenticated.
193
     * @return bool
194
     * @throws RestSystemException
195
     */
196
    protected function isAuthenticated() {
197
        return $this->currentUser() ? true : false;
198
    }
199
200
    /**
201
     * Check if the user has admin privileges.
202
     *
203
     * @return bool
204
     * @throws RestSystemException
205
     */
206
    protected function isAdmin() {
207
        $member = $this->currentUser();
208
        return $member && \Injector::inst()->get('PermissionChecks')->isAdmin($member);
209
    }
210
211
    /**
212
     * @param \SS_HTTPResponse $response the current response object
213
     * @return \SS_HTTPResponse the response with CORS headers
214
     */
215
    protected function addCORSHeaders($response) {
216
        $response->addHeader('Access-Control-Allow-Origin', \Config::inst()->get('BaseRestController', 'CORSOrigin'));
217
        $response->addHeader('Access-Control-Allow-Methods', \Config::inst()->get('BaseRestController', 'CORSMethods'));
218
        $response->addHeader('Access-Control-Max-Age', \Config::inst()->get('BaseRestController', 'CORSMaxAge'));
219
        $response->addHeader('Access-Control-Allow-Headers', \Config::inst()->get('BaseRestController', 'CORSAllowHeaders'));
220
        return $response;
221
    }
222
223
    /**
224
     * Return the current user from the request.
225
     * @return \Member the current user
226
     */
227
    protected function currentUser() {
228
        return AuthFactory::createAuth()->current($this->request);
229
    }
230
}
231