You've probably encountered the “tabs vs. spaces” debate for indentation
before. This argument has been around for ages, and both sides present their
reasons:
Tabs:
- Indenting is their purpose
- Smaller files, as indentation takes up one character
- You can set your own indentation width (more on this later)
Spaces:
- Code will look the same everywhere, and consistency is key
- Avoid potential issues in environments sensitive to whitespace
But what if it's about more than personal preference? ChaseMoskal recently
posted a thought-provoking entry on Reddit titled Nobody
talks about the real reason to use tabs instead of spaces that might open
your eyes.
The Main Reason to Use Tabs
Chase describes his experience with implementing spaces at his workplace and
the negative impacts it had on colleagues with visual impairments.
One of them was accustomed to using a tab width of 1 to avoid large
indentations when using large fonts. Another uses a tab width of 8 because it
suits him best on an ultra-wide monitor. For both, however, code with spaces
poses a serious problem, requiring them to convert spaces to tabs before reading
and back to spaces before committing.
For blind programmers who use Braille displays, each space represents one
Braille cell. Therefore, if the default indentation is 4 spaces, a third-level
indentation wastes 12 precious Braille cells even before the start of the code.
On a 40-cell display, which is most commonly used with laptops, this is more
than a quarter of the available cells, wasted without conveying any
information.
Adjusting the width of indentation may seem trivial to us, but for some
programmers, it is absolutely essential. And that’s something we simply
cannot ignore.
By using tabs in our projects, we give them the opportunity for this
adjustment.
Accessibility First,
Then Personal Preference
Sure, not everyone can be persuaded to choose one side over the other when it
comes to preferences. Everyone has their own. And we should appreciate the
option to choose.
However, we must ensure that we consider everyone. We should respect
differences and use accessible means. Like the tab character, for instance.
I think Chase put it perfectly when he mentioned in his post that
“…there is no counterargument that comes close to outweighing the
accessibility needs of our colleagues.”
Accessible First
Just as the “mobile first” methodology has become popular in web design,
where we ensure that everyone, regardless of device, has a great user experience
with your product – we should strive for an “accessible first”
environment by ensuring that everyone has the same opportunity to work with
code, whether in employment or on an open-source project.
If tabs become the default choice for indentation, we remove one barrier.
Collaboration will then be pleasant for everyone, regardless of their abilities.
If everyone has the same opportunities, we can fully utilize our collective
potential ❤️
This article is based on Default
to tabs instead of spaces for an ‘accessible first’ environment. I read
a similarly convincing post in 2008 and changed from spaces to tabs in all my
projects that very day. It left a trace
in Git, but the article itself has disappeared into the annals of
history.
As of version 3.1.6, the Texy library adds support for Latte
3 in the form of the {texy}
tag. What can it do and how do you
deploy it?
The {texy}
tag represents an easy way to write directly in Texy
syntax in Latte templates:
{texy}
You Already Know the Syntax
----------
No kidding, you know Latte syntax already. **It is the same as PHP syntax.**
{/texy}
Simply install the extension in Latte and pass it a Texy object configured as
needed:
$texy = new Texy\Texy;
$latte = new Latte\Engine;
$latte->addExtension(new Texy\Bridges\Latte\TexyExtension($texy));
If there is static text between the {texy}...{/texy}
tags, it is
translated using Texy during the template compilation and the result is stored
in it. If the content is dynamic (i.e., there are Latte tags inside), the
processing using Texy is performed each time the template is rendered.
If it is desirable to disable Latte tags inside, it can be done
like this:
{texy syntax: off} ... {/texy}
In addition to the Texy object, a custom function can also be passed to the
extension, thus allowing parameters to be passed from the template. For
instance, we might want to pass the parameters locale
and
heading
:
$processor = function (string $text, int $heading = 1, string $locale = 'cs'): string {
$texy = new Texy\Texy;
$texy->headingModule->top = $heading;
$texy->typographyModule->locale = $locale;
return $texy->process($text);
};
$latte = new Latte\Engine;
$latte->addExtension(new Texy\Bridges\Latte\TexyExtension($processor));
Parameters in the template are passed like this:
{texy locale: en, heading: 3}
...
{/texy}
If you want to format text stored in a variable using Texy, you can use a
filter:
{$description|texy}
Please roll out the fanfare as Latte 3 enters the scene with a
completely rewritten compiler. This new version represents the biggest
developmental leap in Nette's history.
Why Latte, Exactly?
Latte has an intriguing history.
Originally, it wasn’t meant to be taken seriously. In fact, it was supposed to
demonstrate that no templating system was needed in PHP. It was tightly
integrated with presenters in Nette, but it wasn’t enabled by default and
programmers had to activate it using its then-awkward name,
CurlyBracketsFilter.
The turning point came with the idea that a templating system could actually
understand HTML pages. Let me explain. For other templating systems, the text
around tags is just noise without any meaning. Whether it's an HTML page, CSS
style, or even text in Markdown, the templating engine only sees a cluster of
bytes. Latte, on the other hand, understands the document. This brings many
significant advantages, from convenience features like n:attributes to
ultimate security.
Latte knows which escaping function to use
(something most programmers don’t know, but thanks to Latte, it doesn’t
matter and doesn’t create a security hole like Cross-site
scripting). It prevents printing strings that could be dangerous in certain
contexts. It can even prevent misinterpretation of mustache
brackets by a frontend framework. And security experts will have nothing to
complain about :)
I wouldn't have expected this idea to put Latte a decade ahead of other
systems, as to this day I only know of two that work this way. Besides Latte,
there’s Google's Soy. Latte and Soy are the only truly secure templating
systems for the web. (Although Soy only has the escaping feature from the
mentioned perks.)
Another key feature of Latte is that for expressions within tags (sometimes
referred to as macros), it uses PHP. Thus, the syntax is familiar to the
programmer. Developers don’t need to learn a new language. They don’t need
to figure out how this or that is written in Latte. They just write it as they
know how. By contrast, the popular templating system Twig uses Python syntax,
where even basic constructs are written differently. For example,
foreach ($people as $person)
is written as
for person in people
in Python (and thus in Twig), which
unnecessarily forces the brain to switch between two opposing conventions.
Thus, Latte adds so much value compared to its competitors that it makes
sense to invest effort in its maintenance and development.
Current Compiler
Latte and its syntax were created 14 years ago (2008), with the current
compiler following three years later. It already knew everything essential that
is still used today, including blocks, inheritance, snippets, etc.
The compiler operated in a single-pass mode, meaning it parsed the template
and directly transformed it into PHP code, which was compiled into the final
file. The PHP language used in the tags (i.e., in macros) was tokenized and then
underwent several processes that modified the tokens. One process added
quotation marks around identifiers, another added syntactic perks that PHP did
not know at the time (such as array writing with []
instead of
array()
, nullsafe operators ?->
) or that are still
unknown (short ternary operator, filters
($var|upper|truncate)
, etc).
These processes did not check PHP syntax or used constructions. This changed
dramatically two years ago (2020) with the introduction of sandbox mode. Sandbox searches for
possible function and method calls in tokens and modifies them, which is not
simple. Any failure here is essentially a security flaw.
New Compiler
In the eleven years since Latte was developed, there were situations where
the single-pass compiler was insufficient (such as when including a block that
was not yet defined).
While all issues could be resolved, it would be ideal to switch to a two-step
compilation, first parsing the template into an intermediate form, the AST tree,
and then generating class code from it.
Also, with the gradual improvement of the PHPlike language used in the
tags, the representation in tokens was no longer sufficient, and it would be
ideal to parse it into an AST tree as well. Programming a sandbox over an AST
tree is significantly easier and guarantees that it will be truly bullet
proof.
It took me five years to get started with rewriting the compiler because
I knew it would be extremely challenging. The mere tokenization of the
template is a challenge, as it must run parallel to parsing. The parser must be
able to influence the tokenization, for example, when it encounters the
attribute n:syntax=off.
Support for parallel execution of two codes is brought by Fibers in PHP 8.1,
however, Latte does not yet use them to be compatible with PHP 8.0. Instead, it
uses similar coroutines (you won’t find documentation about them in PHP
documentation, so here’s a link to Generator RFC). Under the hood,
Latte performs magic.
However, writing a lexer and parser for a language as complex as the PHP
dialect used in the tags seemed even more challenging. Essentially, it meant
creating something like nikic/PHP-Parser for Latte. And also the need to
formalize the grammar of this language.
Today I can say that I've managed to complete everything. Latte has the
compiler I've long wished for. And not a single line of code from the original
remains 🙂
Many years ago, I realized that when I used a variable containing a
predefined data table in a PHP function, the array had to be “recreated”
each time the function was called, which was surprisingly slow. For example:
function isSpecialName(string $name): bool
{
$specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];
return isset($specialNames[$name]);
}
Then I discovered a simple trick that prevented the array from being
recreated. It was enough to define the variable as static:
function isSpecialName(string $name): bool
{
static $specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];
return isset($specialNames[$name]);
}
The speed-up, if the array was a bit larger, was several orders of magnitude
(like 500×).
Since then, I have always used static
for constant arrays.
It's possible that others followed this habit without knowing the real reason
behind it, but I can't be sure.
A few weeks ago, I wrote a class that held large tables of predefined data
in several properties. I realized that this would slow down the creation of
instances, meaning the new
operator would “recreate” the arrays
each time, which is slow as we know. Therefore, I had to change the properties
to static, or perhaps even better, use constants.
Then I asked myself: Hey, are you just following a cargo cult? Is it still true
that without static it is slow?
It's hard to say, PHP has undergone revolutionary development and old truths
may no longer be valid. I prepared a test sample and did a few measurements. Of
course, I confirmed that in PHP 5, using static inside a function or with
properties significantly sped things up by several orders of magnitude. However,
note that in PHP 7.0, it was only by one order of magnitude. Excellent, a sign
of optimizations in the new core, but the difference is still substantial. Yet,
with further PHP versions, the difference continued to decrease and eventually
nearly disappeared.
I even found that using static inside a function in PHP 7.1 and
7.2 actually slowed down the execution by about 1.5–2×, which in terms of
the orders of magnitude we are discussing, is negligible, but it was an
interesting paradox. From PHP 7.3, the difference disappeared completely.
Habits are a good thing, but it is necessary to validate their meaning
continuously.
I will no longer use unnecessary static within function bodies. However, for
that class holding large tables of predefined data in properties, I thought it
was programmatically correct to use constants. Soon, I had the refactoring
done, but even as it was being created, I lamented how ugly the code was
becoming. Instead of $this->ruleToNonTerminal
or
$this->actionLength
, the code now contained the screaming
$this::RULE_TO_NON_TERMINAL
and $this::ACTION_LENGTH
,
which looked really ugly. A stale whiff from the seventies.
I even hesitated, wondering if I even wanted to look at such ugly code, and
whether I might prefer to stick with variables, or static variables.
And then it hit me: Hey, are you just following a cargo cult?
Of course, I am. Why should a constant shout? Why should it draw attention
to itself in the code, be a protruding element in the flow of the program? The
fact that the structure is read-only is not a reason FOR STUCK CAPSLOCK,
AGGRESSIVE TONE, AND WORSE READABILITY.
THE TRADITION OF UPPERCASE LETTERS COMES FROM THE C LANGUAGE, WHERE MACRO
CONSTANTS FOR THE PREPROCESSOR WERE MARKED IN THIS WAY. IT WAS USEFUL TO
UNMISTAKABLY DISTINGUISH CODE FOR THE PARSER FROM CODE FOR THE PREPROCESSOR. IN
PHP, NO PREPROCESSORS WERE EVER USED, SO THERE IS NO REASON to write constants
in uppercase letters.
That very evening, I removed them everywhere. And still couldn't understand
why it hadn't occurred to me twenty years ago. The bigger the nonsense, the
tougher its roots.
I've always been bothered by any redundancy or duplication in code. I wrote about it many years
ago. Looking at this code just makes me suffer:
interface ContainerAwareInterface
{
/**
* Sets the container.
*/
public function setContainer(ContainerInterface $container = null);
}
Let's set aside the unnecessary commentary on the method for now. And this
time also the misunderstanding of dependency injection, if a library needs such
an interface. The fact that using the word Interface
in the name of
an interface is, in turn, a sign of not understanding object-oriented
programming, I'm planning a separate article on that. After all, I've been there
myself.
But why on earth specify the visibility as public
? It's a pleonasm. If it wasn't public,
then it wouldn't be an interface, right? And then someone thought to make it a
“standard” ?♂️
Sorry for the long introduction, what I'm getting to is whether to write
optional nullable types with or without a question mark. So:
// without
function setContainer(ContainerInterface $container = null);
// with
function setContainer(?ContainerInterface $container = null);
Personally, I have always leaned towards the first option, because the
information given by the question mark is redundant (yes, both notations mean
the same from the language's perspective). This is how all the code was written
until the arrival of PHP 7.1, the version that added the question mark, and
there would have to be a good reason to change it suddenly.
With the arrival of PHP 8.0, I changed my mind and I'll explain why. The
question mark is not optional in the case of properties. PHP will throw an error
in this case:
class Foo
{
private Bar $foo = null;
}
// Fatal error: Default value for property of type Bar may not be null.
// Use the nullable type ?Bar to allow null default value
And from PHP 8.0 you can use promoted
properties, which allows you to write code like this:
class Foo
{
public function __construct(
private ?Bar $foo = null,
string $name = null,
) {
// ...
}
}
Here you can see the inconsistency. If ?Bar
is used (which is
necessary), then ?string
should follow on the next line. And if
I use the question mark in some cases, I should use it in all cases.
The question remains whether it is better to use a union type
string|null
instead of a question mark. For example, if I wanted
to write Stringable|string|null
, maybe the version with a question
mark isn't at all necessary.
Update: It looks like PHP 8.4 will require the notation with a
question mark.
I was curious about which PHP framework has the best
documentation and how Nette ranks among them. But how can you find out?
We all know that the worst scenario is having no documentation at all,
followed by inadequate documentation. The opposite is extensive documentation.
It seems, therefore, that the sheer volume of documentation is an important
indicator. Of course, its understandability and currency, as well as readability
and accuracy, play a huge role. These factors are very difficult to measure.
However, I know from my own experience how many sections of
Nette's documentation I have rewritten multiple times to make them clearer,
and how many corrections I have merged, and I assume this happens with any
long-standing framework. Thus, it appears that all documentation gradually
converges towards a similar high quality. Therefore, I allow myself to take the
sheer volume of data as a guide, though it is an oversimplification.
Of course, the volume of documentation must be proportional to the size of
the library itself. Some are significantly larger than others and should
accordingly have significantly more documentation. For simplicity, I determine
the size of the library by the volume of PHP code, normalized for white space
and excluding comments.
I created a chart showing the ratio of English documentation to code for
well-known frameworks CakePHP (4.2), CodeIgniter (3.1), Laravel (8.62), Nette
(3.1), Symfony (5.4), YII (2.0), and Zend Framework (2.x, no longer in
development):
As you can see from the chart, the extent of documentation relative to the
code is more or less similar across all frameworks.
CodeIgniter stands out. I tip my hat to CakePHP and YII, which strive to
maintain documentation in a range of other languages. The comprehensiveness of
Nette's documentation is above average. Additionally, Nette is the only
framework that has a 1:1 translation in our native language.
The purpose of the chart is NOT to show that one framework has so many
percent more comprehensive documentation than another. The metric is too
primitive for that. Instead, the purpose is to show that the extent of
documentation among the various frameworks is largely comparable. I created it
mainly for myself, to get an idea of how Nette's documentation compares to its
competitors.
Originally published in August 2019, data updated for
October 2021.
The shutdown process in PHP consists of the following steps performed in the
given order:
- Calling all functions registered using
register_shutdown_function()
- Calling all
__destruct()
methods
- Emptying all output buffers
- Terminating all PHP extensions (e.g., sessions)
- Shutting down the output layer (sending HTTP headers, cleaning output
handlers, etc.)
Let's focus more closely on step 2, the calling of destructors.
It's important to note that even in the first step, when registered shutdown
functions are called, object destruction can occur. For example, if one of the
functions held the last reference to an object or if the shutdown function
itself was an object.
Destructor calls proceed as follows:
- PHP first attempts to destroy objects in the global symbol table.
- Then it calls the destructors of all remaining objects.
- If execution is halted, e.g., due to
exit()
, the remaining
destructors are not called.
ad 1) PHP iterates through the global symbol table in reverse order, starting
with the most recently created variable and proceeding to the first created
variable. During this iteration, it destroys all objects with a reference count
of 1. This iteration continues as long as such objects exist.
Basically, it does the following: a) removes all unused objects in the global
symbol table, b) if new unused objects appear, removes them as well, and c)
continues this process. This method of destruction is used so that objects can
depend on other objects in their destructor. This usually works well if objects
in the global scope don't have complicated (e.g., circular) mutual
dependencies.
Destruction of the global symbol table is significantly different from the
destruction of other symbol tables. For the global symbol table, PHP uses a
smarter algorithm that tries to respect object dependencies.
ad 2) Other objects are processed in the order they were created, and their
destructors are called. Yes, PHP merely calls __destruct
, but it
doesn't actually destroy the object (nor does it even change its reference
count). If other objects still refer to it, the object will remain available
(even though its destructor has already been called). In a sense, they will be
using a “half-destroyed” object.
ad 3) If execution is halted during the calling of destructors, e.g., due to
exit()
, the remaining destructors are not called. Instead, PHP
marks the objects as already destroyed. The important consequence is that
destructor calls are not guaranteed. While such cases are relatively rare, they
can happen.
Source: https://stackoverflow.com/…ucted-in-php
When writing your own error handler for PHP, it is absolutely
necessary to follow several rules. Otherwise, it can disrupt the behavior of
other libraries and applications that do not expect treachery in the error
handler.
Parameters
The signature of the handler looks like this:
function errorHandler(
int $severity,
string $message,
string $file,
int $line,
array $context = null // only in PHP < 8
): ?bool {
...
}
The $severity
parameter contains the error level
(E_NOTICE
, E_WARNING
, …). Fatal errors such as
E_ERROR
cannot be caught by the handler, so this parameter will
never have these values. Fortunately, fatal errors have essentially disappeared
from PHP and have been replaced by exceptions.
The $message
parameter is the error message. If the html_errors
directive is enabled, special characters like <
are written as
HTML entities, so you need to decode
them back to plain text. However, beware, some characters are not written
as entities, which is a bug. Displaying errors in pure PHP is thus prone to XSS.
The $file
and $line
parameters represent the name
of the file and the line where the error occurred. If the error occurred inside
eval()
, $file
will be supplemented with this information.
Finally, the $context
parameter contains an array of local
variables, which is useful for debugging, but this has been removed in PHP
8. If the handler is to work in PHP 8, omit this parameter or give it a
default value.
Return Value
The return value of the handler can be null
or
false
. If the handler returns null
, nothing happens.
If it returns false
, the standard PHP handler is also called.
Depending on the PHP configuration, this can print or log the error.
Importantly, it also fills in internal information about the last error, which
is accessible by the error_get_last()
function.
Suppressed Errors
In PHP, error display can be suppressed either using the shut-up operator
@
or by error_reporting()
:
// suppress E_USER_DEPRECATED level errors
error_reporting(~E_USER_DEPRECATED);
// suppress all errors when calling fopen()
$file = @fopen($name, 'r');
Even when errors are suppressed, the handler is still called.
Therefore, it is first necessary to verify whether the error is
suppressed, and if so, we must end our own handler:
if (!($severity & error_reporting())) {
return false;
}
However, in this case, we must end it with return false
,
so that the standard error handler is still executed. It will not print or log
anything (because the error is suppressed), but ensures that the error can be
detected using error_get_last()
.
Other Errors
If our handler processes the error (for example, displays its own message,
etc.), there is no need to call the standard handler. Although then it will not
be possible to detect the error using error_get_last()
, this does
not matter in practice, as this function is mainly used in combination with the
shut-up operator.
If, on the other hand, the handler does not process the error for any reason,
it should return false
so as not to conceal it.
Example
Here's what the code for a custom error handler that transforms errors into
ErrorException
exceptions might look like:
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
SameSite cookies provide a mechanism to recognize what led to
the loading of a page. Whether it was through clicking a link on another
website, submitting a form, loading inside an iframe, using
JavaScript, etc.
Identifying how a page was loaded is crucial for security. The serious
vulnerability known as Cross-Site
Request Forgery (CSRF) has been with us for over twenty years, and SameSite
cookies offer a systematic way to address it.
A CSRF attack involves an attacker luring a victim to a webpage that
inconspicuously makes a request to a web application where the victim is logged
in, and the application believes the request was made voluntarily by the victim.
Thus, under the identity of the victim, some action is performed without the
victim knowing. This could involve changing or deleting data, sending a message,
etc. To prevent such attacks, applications need to distinguish whether the
request came from a legitimate source, e.g., by submitting a form on the
application itself, or from elsewhere. SameSite cookies can do this.
How does it work? Let’s say I have a website running on a domain, and
I create three different cookies with attributes SameSite=Lax
,
SameSite=Strict
, and SameSite=None
. Name and value do
not matter. The browser will store them.
- When I open any URL on my website by typing directly into the address bar
or clicking on a bookmark, the browser sends all three cookies.
- When I access any URL on my website from a page from the same
website, the browser sends all three cookies.
- When I access any URL on my website from a page from a different
website, the browser sends only the cookies with
None
and in
certain cases Lax
, see table:
Code on another website |
|
Sent cookies |
Link |
<a href="…"> |
None + Lax |
Form GET |
<form method="GET" action="…"> |
None + Lax |
Form POST |
<form method="POST" action="…"> |
None |
iframe |
<iframe src="…"> |
None |
AJAX |
$.get('…'), fetch('…') |
None |
Image |
<img src="…"> |
None |
Prefetch |
<link rel="prefetch" href="…"> |
None |
… |
|
None |
SameSite cookies can distinguish only a few cases, but these are crucial for
protecting against CSRF.
If, for example, there is a form or a link for deleting an item on my
website's admin page and it was sent/clicked, the absence of a cookie created
with the Strict
attribute means it did not happen on my website but
rather the request came from elsewhere, indicating a CSRF attack.
Create a cookie to detect a CSRF attack as a so-called session cookie without
the Expires
attribute, its validity is essentially infinite.
Domain vs Site
“On my website” is not the same as “on my domain,” it's not about
the domain, but about the website (hence the name SameSite). Although the site
often corresponds to the domain, for services like github.io
, it
corresponds to the subdomain. A request from doc.nette.org
to
files.nette.org
is same-site, while a request from
nette.github.io
to tracy.github.io
is already
cross-site. Here it is nicely
explained.
<iframe>
From the previous lines, it is clear that if a page from my website is loaded
inside an <iframe>
on another website, the browser does not
send Strict
or Lax
cookies. But there's another
important thing: if such a loaded page creates Strict
or
Lax
cookies, the browser ignores them.
This creates a possibility to defend against fraudulent acquisition of
cookies or Cookie
Stuffing, where until now, systemic defense was also lacking. The trick is
that the fraudster collects a commission for affiliate marketing, although the
user was not brought to the merchant's website by a user-clicked link. Instead,
an invisible <iframe>
with the same link is inserted into the
page, marking all visitors.
Cookies without the SameSite
Attribute
Cookies without the SameSite attribute were always sent during both same-site
and cross-site requests. Just like SameSite=None
. However, in the
near future, browsers will start treating the SameSite=Lax
flag as
the default, so cookies without an attribute will be considered
Lax
. This is quite an unusually large BC break in browser behavior.
If you want the cookie to continue to behave the same and be transmitted during
any cross-site request, you need to set it to SameSite=None
.
(Unless you develop embedded widgets, etc., you probably won't want this often.)
Unfortunately, for last year's browsers, the None
value is
unexpected. Safari 12 interprets it as Strict
, thus creating a
tricky problem on older iOS and macOS.
And note: None
works only when set with the Secure
attribute.
What to Do in Case of an
Attack?
Run away! The basic rule of self-defense, both in real life and on the web.
A huge mistake made by many frameworks is that upon detecting a CSRF attack,
they display the form again and write something like “The CSRF token is
invalid. Please try to submit the form again”. By resubmitting the form,
the attack is completed. Such protection lacks sense when you actually invite
the user to bypass it.
Until recently, Chrome did that during a cross-site request—it displayed
the page again after a refresh, but this time sent the cookies with the
Strict
attribute. So, the refresh eliminated the CSRF protection
based on SameSite cookies. Fortunately, it no longer does this today, but
it's possible that other or older browsers still do. A user can also
“refresh” the page by clicking on the address bar + enter, which is
considered a direct URL entry (point 1), and all cookies are sent.
Thus, the best response to detecting CSRF is to redirect with a 302 HTTP
code elsewhere, perhaps to the homepage. This rids you of dangerous POST data,
and the problematic URL isn't saved to history.
Incompatibilities
SameSite hasn't worked nearly as well as it should have for a long time,
mainly due to browser bugs and deficiencies in the specification, which, for
example, didn't address redirections or refreshes. SameSite cookies weren't
transferred during saving or printing a page, but were transferred after a
refresh when they shouldn't have been, etc. Fortunately, the situation is better
today. I believe that the only serious shortcomings in current browser versions
persist, as mentioned above for Safari.
Addendum: Besides SameSite, the origin of a request can very recently be
distinguished also by the Origin
header, which is more privacy-respecting and more accurate than the Referer
header.
Content Security Policy (CSP) is an additional security feature that tells
the browser what external sources a page can load and how it can be displayed.
It protects against the injection of malicious code and attacks such as XSS. It
is sent as a header composed of a series of
directives. However, implementing it is not trivial.
Typically, we want to use JavaScript libraries located outside our server,
such as Google Analytics, advertising systems, captchas, etc. Unfortunately, the
first version of CSP fails here. It requires a precise analysis of the content
loaded and the setting of the correct rules. This means creating a whitelist, a
list of all the domains, which is not easy since some scripts dynamically pull
other scripts from different domains or are redirected to other domains, etc.
Even if you take the effort and manually create the list, you never know what
might change in the future, so you must constantly monitor if the list is still
up-to-date and correct it. Analysis by Google showed that even this meticulous
tuning ultimately results in allowing such broad access that the whole purpose
of CSP falls apart, just sending much larger headers with each request.
CSP level 2 approaches the problem differently using a nonce, but only the
third version of the solution completed the process. Unfortunately, as of 2019,
it does not have sufficient browser support.
Regarding how to assemble the script-src
and
style-src
directives to work correctly even in older browsers and
to minimize the effort, I have written a detailed
article in the Nette partner section. Essentially, the resulting form might
look like this:
script-src 'nonce-XXXXX' 'strict-dynamic' * 'unsafe-inline'
style-src 'nonce-XXXXX' * 'unsafe-inline'
Example of Use in PHP
We generate a nonce and send the header:
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic' * 'unsafe-inline'");
And we insert the nonce into the HTML code:
<script nonce="<?=$nonce?>" src="..."></script>
Example of Use in Nette
Since Nette has built-in support for CSP and nonce since version 2.4, simply
specify in the configuration
file:
http:
csp:
script-src: [nonce, strict-dynamic, *, unsafe-inline]
style-src: [nonce, *, unsafe-inline]
And then use in templates:
<script n:nonce src="..."></script>
<style n:nonce>...</style>
Monitoring
Before you set new rules for CSP, try them out first using the
Content-Security-Policy-Report-Only
header. This header works in
all browsers that support CSP. If a rule is violated, the browser does not block
the script but instead sends a notification to the URL specified in the
report-uri
directive. To receive and analyze these notifications,
you might use a service like Report
URI.
http:
cspReportOnly:
script-src: [nonce, strict-dynamic, *, unsafe-inline]
report-uri: https://xxx.report-uri.com/r/d/csp/reportOnly
You can use both headers simultaneously, with
Content-Security-Policy
having verified and active rules and
Content-Security-Policy-Report-Only
to test their modifications. Of
course, you can also monitor failures in the strict rules.