I ran into an issue with CodeIgniter / CSRF / JSON.
I am sending http POST requests to my PHP backend with the Content-Type "application/json. The payload is JSON data. Along with the data, I pass the CSRF token that is generated and stored in the CSRF cookie. With a standard POST FORM request, it works just fine, but when sending as JSON it fails.
As $_POST array is empty because of the JSON content-type, CodeIgniter fails to validate the cookie and throws an error.
How can I have CodeIgniter check JSON payload and validate my CSRF token ?
To fix that issue, I had to change the code of the "Security.php" file located in "system/core/".
In function "csrf_verify", replace that code:
// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]))
{
$this->csrf_show_error();
}
// Do the tokens match?
if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
{
$this->csrf_show_error();
}
By that code:
// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])) {
// No token found in $_POST - checking JSON data
$input_data = json_decode(trim(file_get_contents('php://input')), true);
if ((!$input_data || !isset($input_data[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])))
$this->csrf_show_error(); // Nothing found
else {
// Do the tokens match?
if ($input_data[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
$this->csrf_show_error();
}
}
else {
// Do the tokens match?
if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
$this->csrf_show_error();
}
That code first checks $_POST then if nothing has been found, it checks the JSON payload.
The ideal way of doing this would be to check the incoming request Content-Type header value. But surprisingly, it's not straight forward to do ...
If someone has a better solution, please post it here.
Cheers
Alternatively, you can skip the CSRF checking by adding following code on application/config/config.php below Line No. 351 (based on CI 2.1.4).
$config['csrf_expire'] = 7200; // This is line no. 351
/* If the REQUEST_URI has method is POST and requesting the API url,
then skip CSRF check, otherwise don't do. */
if (isset($_SERVER["REQUEST_URI"]) &&
(isset($_SERVER['REQUEST_METHOD']) && ($_SERVER['REQUEST_METHOD'] == 'POST') ))
{
if (stripos($_SERVER["REQUEST_URI"],'/api/') === false ) { // Verify if POST Request is not for API
$config['csrf_protection'] = TRUE;
}
else {
$config['csrf_protection'] = FALSE;
}
} else {
$config['csrf_protection'] = TRUE;
}
If this needs to be overridden, best to extend the Security library rather than editing the core file directly.
Create the file My_Security.php in application/core/ and add the following (from the solution above):
<?php
class My_Security extends CI_Security {
/**
* Verify Cross Site Request Forgery Protection
*
* #return object
*/
public function csrf_verify()
{
// If it's not a POST request we will set the CSRF cookie
if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
{
return $this->csrf_set_cookie();
}
// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])) {
// No token found in $_POST - checking JSON data
$input_data = json_decode(trim(file_get_contents('php://input')), true);
if ((!$input_data || !isset($input_data[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])))
$this->csrf_show_error(); // Nothing found
else {
// Do the tokens match?
if ($input_data[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
$this->csrf_show_error();
}
}
else {
// Do the tokens match?
if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
$this->csrf_show_error();
} // We kill this since we're done and we don't want to
// polute the _POST array
unset($_POST[$this->_csrf_token_name]);
// Nothing should last forever
unset($_COOKIE[$this->_csrf_cookie_name]);
$this->_csrf_set_hash();
$this->csrf_set_cookie();
log_message('debug', 'CSRF token verified');
return $this;
}
}
As Brian write, you have to put your custom class into /application/core/ ex. My_Security.php
This is mine solution, work for me, i check the application/json content_type and request cookies.
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Security extends CI_Security {
public function csrf_verify()
{
// If it's not a POST request we will set the CSRF cookie
if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
{
return $this->csrf_set_cookie();
}
/**
* mine implementation for application/json
*/
$reqHeaders = getallheaders();
$content_type = $reqHeaders["Content-Type"];
#it's a json request?
if(preg_match("/(application\/json)/i",$content_type))
{
#the check the cookie from request
$reqCookies = explode("; ",$reqHeaders["Cookie"]);
foreach($reqCookies as $c)
{
if(preg_match("/(".$this->_csrf_cookie_name."\=)/", $c))
{
$c = explode("=",$c);
if($_COOKIE[$this->_csrf_cookie_name] == $c[1])
{
return $this;
}
}
}
}
//< end
// Check if URI has been whitelisted from CSRF checks
if ($exclude_uris = config_item('csrf_exclude_uris'))
{
$uri = load_class('URI', 'core');
foreach ($exclude_uris as $excluded)
{
if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string()))
{
return $this;
}
}
}
// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match?
{
$this->csrf_show_error();
}
// We kill this since we're done and we don't want to polute the _POST array
unset($_POST[$this->_csrf_token_name]);
// Regenerate on every submission?
if (config_item('csrf_regenerate'))
{
// Nothing should last forever
unset($_COOKIE[$this->_csrf_cookie_name]);
$this->_csrf_hash = NULL;
}
$this->_csrf_set_hash();
$this->csrf_set_cookie();
log_message('info', 'CSRF token verified');
return $this;
}
}
Related
I am wrote API method, after calling that method , I got my response like
[
{
"spark_version": "7.6.x-scala2.12"
}
]
Now I want to have variable in my API method which store value 7.6.x-scala2.12.
API Controller method
[HttpGet]
public IActionResult GetTest(int ActivityId)
{
string StoredJson = "exec sp_GetJobJSONTest " +
"#ActivityId = " + ActivityId ;
var result = _context.Test.FromSqlRaw(StoredJson);
return Ok(result);
}
So how this variable should call on this response to get string stored in spark_version?
Thank you
As you have the JavaScript tag, here's how you'd do it in JS:
If you are able to call your API method, then you can just assign the response to a variable. For example, if you are calling it using the fetch API, it would look something like:
let apiResponse;
fetch('myApiUrl.com').then((response) => {
if (response.status === 200) {
apiResponse = response.body;
console.log('Response:', apiResponse[0]['spark_version']);
}
});
(I defined the variable outside the then to make it globally accessible)
I have created a service which sends a request to the backend and makes the result available to the component via an observable.
export class QuestionManagementService {
questionsArray$: Observable<Result>;
private questionsArraySubject: BehaviorSubject<Result>; //to send list of questions
...
constructor(private http: HttpClient, private helper:HelperService, private bs:WebToBackendInterfaceService, private loaderService:LoaderService) {
this.questionsArraySubject = new BehaviorSubject<Result>(new Result('initial',{})); //A Subject can act both as an Observable and an Observer
this.questionsArray$ = this.questionsArraySubject.asObservable(); //create Observable. Other components can subcribe to it now to get notifications/values
...
}
//this method sends the request to network via another `bs` service. The request is sent using `http.post`
getQuestions(questionFilter:GetQuestionsfilter){
console.log("In QuestionManagementService: getQuestions");
let observable:Observable<HttpEvent<any>> = this.bs.getQuestions(questionFilter);
let subscription:Subscription = observable.subscribe((ev:HttpEvent<any>)=>{
if(ev.type === HttpEventType.Response) { //null means that the response wasn't an HttpResponse but probably some internal Rxjs event (eg type 0)
let response= <HttpResponse<any>>ev;
console.log("http response received: ",response);
//should remove the token from storage
console.log('response body from server: ',ev.body);
let isResponseStructureOK: boolean = this.helper.validateServerResponseStructure(ev.body);
if (isResponseStructureOK) {
console.log("response structure is OK");
let response: ServerResponseAPI = ev.body;
let result:string = response.result;
console.log("result is : " + result);
/*if result could be success or error*/
/*additionalInformation is a string and the string contains a valid json which has array of questions
in format {"questions-list":[{"tag":"some tag1","description":"some description1"},{{"tag":"some tag2","description":"some description2"},...]}
*/
let message:string = response['additional-info'];
console.log("message is "+message);
if(result === "success") {
let jsonQuestionList: string = response['additional-info'];
console.log("jsonQuestionList response as string: ", jsonQuestionList);
//let jsonQuestions: PracticeQuestionsListAPI = JSON.parse(jsonQuestionList);
//console.log("jsonQuestion array:", jsonQuestions);
//this.questionsArraySubject.next(jsonQuestions['questions-list']);
this.questionsArraySubject.next(new Result('success', response["additional-info"]));
} else {
this.questionsArraySubject.next(new Result('error', response["additional-info"]));
}
}
else {
/**
* If something goes wrong, send error rather than send next with result="error"
*/
console.log("received incorrect response structure from server: ", ev.body);
//TODOM - need to change hard coded responses and pick them from a config or global variable.
this.questionsArraySubject.error(new Result('error',"Invalid response structure from server"));
}
}
else {
console.log("not response. ignoring");
}
},
(error:ServerResponseAPI)=>{/*web to backend service will send error in ServerResponseAPI format. This is what handleError throws*/
console.log("got error from the Observable: ",error);
this.questionsArraySubject.error(new Result('error',error['additional-info']));
},
()=>{ //observable complete
console.log("observable completed")
});
}
}
The following component subscribes to this service.
export class PraticeQuestionListComponent implements OnInit, OnDestroy {
questions: PracticeQuestionsListAPI; //the result from observable will be stored here.
questionListSubscription:Subscription; //reference of the subscription
ngOnDestroy(): void {
console.log("destroying component. unsubscribing");
this.questionListSubscription.unsubscribe()
}
//on initialisation, I subscribe to the observable
ngOnInit(){
console.log("in question list on init. question is ",this.questions);
...
this.questions= new PracticeQuestionsListAPI(new AdditionalPagingInfo("",new PartitionInfo(0,0)),
[]);
let tagSubscription = this.questionManagementService.getSupportedTags(new TagId("coding"));
console.log("subscribing to question mgmt service");
this.questionListSubscription = this.questionManagementService.questionsArray$.subscribe((result:Result)=>{
console.log('received result from question mgmgt service - array observable',result);
if(result.result === "success") { //received response from server
let questionList = JSON.parse(result.additionalInfo) as PracticeQuestionsListAPI;
console.log("got list of questions value ", questionList);
this.questions['pagination-info'] = questionList['pagination-info'];
this.questions['questions-list'] = questionList['questions-list'];
/*
0 length of questions-list means no questions.
this could be response from the server indicating that there are no more questions
*/
/*
* the server indicates that there are no more questions by either sending empty question list or by sending
* 0 values for pagination state and partition info
*/
if (questionList["questions-list"].length !== 0) { //server has send list of questions
this.questions['pagination-info']['page-state'] = questionList['pagination-info']['page-state'];
this.questions['pagination-info']['partition-info'] = questionList['pagination-info']['partition-info'];
this.questions['questions-list'] = questionList['questions-list'];
console.log("previous question filter is ",this.questionsFilter);
this.questionsFilter["pagination-info"]["page-state"]=questionList["pagination-info"]["page-state"];
this.questionsFilter["pagination-info"]["partition-info"].month=questionList["pagination-info"]["partition-info"].month;
this.questionsFilter["pagination-info"]["partition-info"].year=questionList["pagination-info"]["partition-info"].year;
console.log("new question filter is ",this.questionsFilter);
//TODOM - maybe this assignment below was causing memory leak. So changed this as above
//this.questionsFilter['pagination-info'] = questionList['pagination-info'];
this.lastPage = false; //the server indicates that there are no more questions by sending these values (no paging state and no partition info)
if (this.questions['pagination-info']['page-state'].length == 0 &&
this.questions['pagination-info']['partition-info'].year == 0 &&
this.questions['pagination-info']['partition-info'].month == 0) {
this.lastPage = true;
} else {//if the list is empty then there are no (more) questions for the selected tag
this.lastPage = false;
}
} else {
this.lastPage = true; //Don't show next button if there are no questions.
this.showDialog(new PracticeQuestionListContext("Reached end of the search. No more results available", new PracticeQuestionListAdditionalInfo()));
}
} else {
//TODOM - I should probably display the error in case there is an error from the server
console.log("ignoring value");
}
},
(err:Result)=>{
console.log("received error from QuestionArray observable",err);
//TODOM - probably should change the name of DialogContext to Component specific additional context
this.showDialog(new PracticeQuestionListContext(err.additionalInfo,new PracticeQuestionListAdditionalInfo()));
},
()=>{
console.log("question mgmt service, questionarray observable completed.");
});
}
}
The issue I am facing is that if I visit the component for the first time, I get the values from the observable after making some selections in the UI (expected behavior). Then i visit the home page of the application, the component gets destroyed (again, expected behavior). Then if I visit the component again, the observable sends the old values (from the first visit) even when I have not made any UI selections.
Why is the observable sending old values and how can I stop it from doing that? I have created another question in SO with pictures which might explain the scenario better
angular component is retaining old value maybe because the observable is resending past data
This is the function in the cloud as a function:
/**
* Responds to any HTTP request that can provide a "message" field in the body.
*
* #param {!Object} req Cloud Function request context.
* #param {!Object} res Cloud Function response context.
*/
exports.helloWorld = function helloWorld(req, res) {
// Example input: {"message": "Hello!"}
if (req.body.message === undefined) {
// This is an error case, as "message" is required.
res.status(400).send('No message defined!');
} else {
// Everything is okay.
console.log(req.body.message);
res.status(200).send('Success: ' + req.body.message);
}
};
How do I add the JSON to the end of the URL to pass the message to the function...
The URL for the function is
https://us-central1-internal-156105.cloudfunctions.net/myapp
use the"end"
res.end(req.body.message)
So I want to allow users to either request .xml or .json responses when they run a rest API request to the server. (much like twitter)
But I don't belive that the following way is the best way, as it means duplicate code, surely there is a better way to allow .xml or .json response.
$app->get('/books/:id.xml', function ($id) use ($app) {
$app->render('/xml/books.xml', array('id' => $id));
});
$app->get('/books/:id.json', function ($id) use ($app) {
$app->render('json/books.json', array('id' => $id));
});
OR
// Define app routes
$app->get('/hello/{name}.{type}', function ($request, $response, $args) {
//return $response->write("Hello " . $args['name']);
if($args['type'] == 'xml')
{
return 'this is xml';
}
var_dump(parse_url($_SERVER['REQUEST_URI']));
});
if anyone knows how to do this, that would be great.
Consider using value of Accept HTTP header of request instead of file extension in the end of the URI.
I'd say using header is more reliable and more "proper" way to determine the format in which the data should be returned.
URI should be used to point to specific resource. Accept header should be sent by client to tell you what format the data should be returned.
Apart from being standard way to implement a RESTful service, it removes the headache of altering routes (like in your second example).
If you agree to such implementation, there's an excellent library to solve your problem.
It has lots of uses, and here is an example for your use case:
<?php
use Negotiation\Negotiator;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
class YourController
{
public function __construct(Negotiator $negotiator, DataProvider $someDataProvider)
{
$this->negotiator = $negotiator;
$this->someDataProvider = $someDataProvider;
}
/**
* Processing request.
*
* We get data, then we use Negotiator to detect what format the requestor prefers.
* Then we return data in requested format or in case format is not supported,
* fall back to JSON.
*
*
* #param Request $request
* #param Response $response
* #return Response
*/
public function __invoke(Request $request, Response $response)
{
$data = $this->someDataProvider->getSomeData();
$mediaType = $this->determineMediaType($request);
switch ($mediaType) {
case 'application/json':
default:
// $data = $data->asJson();
// transform data to JSON...
break;
case 'application/xml':
$data = $data->asXml();
// transform data to XML...
break;
}
// Write data to body of response
$response->getBody()->write($data);
// Set appropriate response header
return $response->withHeader('Content-Type', $mediaType);
}
/**
* Find preferred data format from Accept header.
*
* Uses Negotiator to determine whether JSON or XML should be returned.
*
* #param Request $request
* #return string
*/
private function determineMediaType(Request $request)
{
$acceptHeader = $this->extractAcceptHeader($request);
// Set list of "known" formats, i.e. formats that your service supports
$known = ['application/json', 'application/xml'];
// Let negotiator determine what format should be used
$mediaType = $this->negotiator->getBest($acceptHeader, $known);
if ($mediaType) {
return $mediaType->getValue();
} else {
return 'application/json'; # if request has unexpected value of accept header, default to JSON
}
}
/**
* Extract Accept header value from Request object
* #param Request $request
* #return string
*/
private function extractAcceptHeader(Request $request)
{
return $request->getHeaderLine('Accept');
}
}
This class is an example of a callback to a route. Such implementation allows you to easily extend the list of supported formats without tampering routes.
I am writing a web API with codeigniter, here is one of my result:
http://manage.pineconetassel.com/index.php/api/v1/colors2
It looks close to right, but if I feed it into hurl.it (GET testing service, sorry cannot post url here)
it cannot be parsed, correctly. it shows "colors", but no values
To compare, the following JSON can be parsed correctly by hurl.it
http://www.w3schools.com//website/Customers_JSON.php
what is wrong with my code? The code generating this API is the following:
model:
public function get_colors2(){
$q = $this->publicDB->select('color')->get('car_colors');
if ($q->num_rows > 0) {
return $q->result_array();
} else {return FALSE;}
}
I am using Phil Sturgeon's RESTful server implementation
controller:
function colors2_get()
{
$result = $this->api_model->get_colors2();
if ($result) {
$this->response($result,200);
} else {$this->response(NULL,404);}
}