DEV Community

Cover image for The Security Problems Hiding Inside ‘Normal’ Frontend Code
Tarunya Kesharwani
Tarunya Kesharwani

Posted on

The Security Problems Hiding Inside ‘Normal’ Frontend Code

For a long time, my understanding of frontend security was basically:

Don't expose API keys.

Don't do something stupid with passwords.

Use HTTPS.

Done :))

Turns out...

absolutely not XD

While working on production code, I started noticing something uncomfortable.

A lot of security vulnerabilities don't look like security vulnerabilities.

They look like completely normal code.

A line displaying HTML.

A hidden admin button.

A form validator.

A token stored in the browser.

An environment variable.

An API request.

Nothing is flashing red.

VS Code isn't screaming.

The application works perfectly.

And that's exactly what makes some of these problems dangerous ._.

So this isn't going to be another:

"Here are 10 OWASP vulnerabilities you should memorize."

I want to show you something more useful:

how security changes the way you read ordinary frontend code.

And more importantly, for every problem we'll answer three questions:

  1. Why is this actually dangerous?
  2. What should we do instead?
  3. What should you learn so you can recognize it yourself?

Because telling someone:

"YOUR APPLICATION IS VULNERABLE!!!"

and then disappearing is not particularly helpful XD

Let's break some things.

Preferably theoretically.


1. The Browser Is Not Your Trusted Environment

This is probably the most important rule in this entire article.

If code reaches someone's browser...

they control that environment.

They can open DevTools.

Modify JavaScript.

Change HTML.

Modify requests.

Call your API directly.

Change values in browser storage.

Disable your frontend validation.

Send requests your UI never allows.

Your beautiful Angular interface is not a security boundary :')

This sounds obvious after someone says it.

But it has consequences everywhere.

Consider:

if (user.role === 'admin') {
  showAdminPanel();
}
Enter fullscreen mode Exit fullscreen mode

Perfectly reasonable frontend code.

But if your backend assumes:

"Well... only admins can see the button."

we have a problem.

An attacker doesn't need your button.

They need your endpoint.

If this exists:

DELETE /api/admin/repositories/123
Enter fullscreen mode Exit fullscreen mode

they can attempt to call it directly.

Your carefully hidden button is sitting there feeling extremely secure while the attacker completely ignores it XD

So what should you do?

Use frontend authorization for user experience.

Use backend authorization for security.

The frontend can decide:

Should I display this action?

The backend must independently decide:

Is this authenticated user actually allowed to perform this action?

Every sensitive endpoint should verify authorization server-side.

Learn next

Study:

  • Authentication vs authorization
  • Role-Based Access Control (RBAC)
  • Permission-based authorization
  • HTTP status codes 401 vs 403
  • Server-side authorization guards/middleware

If you're using NestJS, specifically learn Guards and authorization patterns.


2. Disabled Buttons Are Also Not Security XD

Suppose we have:

<button disabled>
  Delete Everything
</button>
Enter fullscreen mode Exit fullscreen mode

Attacker:

oh no :(

Anyway...

Open DevTools.

Remove disabled.

Or skip the interface completely.

Send the HTTP request manually.

The same applies to logic like:

if (!isAdmin) {
  return;
}
Enter fullscreen mode Exit fullscreen mode

Again:

Good UX.

Not sufficient security.

The browser belongs to the user.

Anything enforced exclusively there can potentially be modified or bypassed.

The rule

Whenever you write a frontend restriction, ask:

"What happens if someone completely ignores this frontend?"

If the answer is:

"They can delete the database."

we may have discovered a slight architectural issue :')


3. “But The Data Came From Our API!”

This one changed how I think about data.

Imagine:

this.http.get('/api/profile').subscribe(profile => {
  this.bio = profile.bio;
});
Enter fullscreen mode Exit fullscreen mode

Then somewhere:

<div [innerHTML]="bio"></div>
Enter fullscreen mode Exit fullscreen mode

At first glance:

The backend sent it.

Therefore it's trusted.

Right?

Not necessarily.

Where did the backend get bio?

Maybe:

User
  ↓
Database
  ↓
API
  ↓
Frontend
  ↓
HTML
Enter fullscreen mode Exit fullscreen mode

The data took a lovely vacation through your entire technology stack...

but nobody actually made it trustworthy XD

The database storing malicious input doesn't sanitize it.

JSON serialization doesn't sanitize it.

HTTPS doesn't sanitize it.

Your API returning it doesn't sanitize it.

This leads to a security concept worth remembering:

Trust the provenance, not the transport.

Ask:

Where did this data originally come from?

Not:

"Which API returned it?"

If users, third-party APIs, URL parameters, imported files, GitHub data, CMS content, or another untrusted source can influence the value, treat it accordingly.


4. XSS Is Much More Interesting Than alert("hacked")

Most beginner explanations of Cross-Site Scripting show:

<script>alert("hacked")</script>
Enter fullscreen mode Exit fullscreen mode

And honestly...

that makes XSS look almost adorable XD

Nobody cares if an attacker can display a popup saying "hacked."

The real problem is that malicious JavaScript may execute inside your application's origin and user context.

Depending on the application and its defenses, that can potentially allow an attacker to:

  • Read data accessible to JavaScript
  • Manipulate the interface
  • Make authenticated actions as the victim
  • Read browser storage accessible to JavaScript
  • Exfiltrate accessible information
  • Display phishing interfaces inside your legitimate application

Imagine logging into a trusted website...

and the website itself shows you a fake:

Session expired. Please enter your password again.

Except that UI was injected by an attacker.

Suddenly alert("hacked") doesn't seem like a particularly useful demonstration :')


5. Learn to Think in Sources and Sinks

Here's a security concept I wish frontend tutorials introduced much earlier.

Source

Where potentially untrusted data enters.

Examples:

URL parameters
Form inputs
Third-party APIs
Database content originally supplied by users
GitHub/API responses
localStorage
postMessage
CMS content
Enter fullscreen mode Exit fullscreen mode

Sink

Somewhere that data becomes dangerous depending on how it's used.

For example:

<div [innerHTML]="content"></div>
Enter fullscreen mode Exit fullscreen mode

The important question becomes:

SOURCE
   ↓
transformations
   ↓
more transformations
   ↓
SINK
Enter fullscreen mode Exit fullscreen mode

Can an attacker control the source?

If yes:

What happens when their input reaches the sink?

That way of thinking is far more useful than memorizing a list of dangerous functions.


6. Your Framework Is Probably Protecting You More Than You Realize

Modern frameworks provide useful security defaults.

For example, Angular escapes/interprets interpolated values safely in normal templates.

Something like:

<p>{{ username }}</p>
Enter fullscreen mode Exit fullscreen mode

is fundamentally different from deliberately injecting arbitrary HTML.

Frameworks are trying to help.

Then sometimes developers do this:

bypassSecurityTrustHtml(content)
Enter fullscreen mode Exit fullscreen mode

Angular:

I don't trust this HTML.

Developer:

Don't worry bro.

Angular:

...okay ._.

XD

The name bypassSecurityTrustHtml is practically Angular asking you to sign a waiver.

There are legitimate situations where trusted HTML needs special handling.

But bypassing framework security means you have accepted responsibility for establishing that the value is actually safe.

Prevention

Prefer normal framework rendering whenever possible.

If you genuinely need user-controlled rich HTML, use a well-maintained sanitizer designed for that purpose and configure it for your threat model rather than trying to remove suspicious strings yourself.

Do NOT invent:

html.replace('<script>', '')
Enter fullscreen mode Exit fullscreen mode

and declare victory XD

Attack payloads have considerably more creativity than that.

Learn next

Study:

  • Cross-Site Scripting (XSS)
  • DOM-based XSS
  • Output encoding
  • HTML sanitization
  • Angular security model / React escaping behavior
  • OWASP XSS Prevention Cheat Sheet
  • DOMPurify or an equivalent maintained sanitizer when arbitrary HTML must be rendered

7. Frontend Validation Is Not Security Validation

You build a beautiful Angular form:

name: new FormControl('', [
  Validators.required,
  Validators.maxLength(100)
])
Enter fullscreen mode Exit fullscreen mode

Fantastic.

Users cannot submit names longer than 100 characters.

Except...

attackers are under no contractual obligation to use your Angular form XD

They can directly send:

POST /api/users
Enter fullscreen mode Exit fullscreen mode

with whatever payload they want.

Frontend validation is still important.

It gives immediate feedback.

Reduces accidental invalid requests.

Makes forms pleasant to use.

But:

Client validation improves user experience. Server validation establishes trust.

Your backend must validate the request again.

Types.

Lengths.

Formats.

Allowed values.

Required properties.

Unexpected properties where relevant.

Business constraints.

And validation should happen before untrusted data reaches sensitive operations.

Learn next

Study:

  • DTO validation
  • Schema validation
  • Input validation
  • Allowlisting vs blocklisting
  • NestJS ValidationPipe
  • class-validator
  • OWASP Input Validation Cheat Sheet

8. Let's Talk About localStorage

Ah yes.

The internet's favourite security debate :')

You'll often hear:

NEVER STORE TOKENS IN LOCALSTORAGE!!!!

And then:

COOKIES ARE SECURE!!!!

Security is rarely that simple.

localStorage is accessible to JavaScript running on the same origin.

Therefore, if malicious JavaScript successfully executes through an XSS vulnerability, values accessible through localStorage may also become accessible to that script.

An HttpOnly cookie, by contrast, cannot be directly read through JavaScript.

That's useful protection against token theft through JavaScript.

But cookies introduce considerations of their own.

For authentication cookies, you need to understand things such as:

  • HttpOnly
  • Secure
  • SameSite
  • CSRF
  • Cookie scope
  • Session expiration
  • Session invalidation

The lesson isn't:

localStorage bad.

cookies good.

The lesson is:

Understand what you're protecting against.

That's threat modeling.

Questions worth asking

If authentication credentials are stored here:

  • Can JavaScript access them?
  • What happens during an XSS attack?
  • Can another origin cause authenticated requests?
  • How does logout invalidate the session?
  • What happens when credentials expire?
  • Can compromised credentials be revoked?

Those questions lead to better architecture than memorizing Twitter security advice XD

Learn next

Study:

  • Cookie-based authentication
  • Session authentication
  • HttpOnly, Secure, and SameSite
  • CSRF
  • XSS
  • Token rotation
  • Session invalidation
  • OWASP Session Management Cheat Sheet

9. Your .env File Does Not Make Frontend Secrets Secret

This one hurts because .env feels extremely secure.

Look at it.

It even has a dot at the beginning.

Very mysterious :0

Suppose:

SUPER_SECRET_API_KEY=abc123
Enter fullscreen mode Exit fullscreen mode

Then your frontend build includes that value.

Congratulations.

Your secret is now...

public :')

If the browser needs a value to execute your application, assume the user can obtain that value.

Minification doesn't change that.

Obfuscation doesn't create a security boundary.

Renaming:

SECRET_API_KEY
Enter fullscreen mode Exit fullscreen mode

to:

x7Q_a19
Enter fullscreen mode Exit fullscreen mode

does not defeat someone determined to inspect your application XD

Important distinction

Not every API key is necessarily secret.

Some services intentionally provide publishable client keys designed to exist in browser applications.

The important question is:

What authority does possession of this value provide?

If the key grants privileged server-side access, signs trusted requests, bypasses authorization, or gives access to private infrastructure...

it does not belong in frontend code.

Prevention

Keep actual secrets server-side.

Frontend:

Browser
   ↓
Your Backend
   ↓
Privileged External Service
Enter fullscreen mode Exit fullscreen mode

The backend keeps the secret and performs authorization before accessing privileged services.

Learn next

Study:

  • Public vs secret API keys
  • Environment variables in frontend build systems
  • Backend-for-Frontend patterns
  • Secret management
  • API key restrictions
  • Least privilege

And remember:

Anything shipped to the browser should be treated as observable by the user.


10. Rate Limiting Is Not Disabling a Button for Five Seconds

Suppose your frontend does:

if (requestInProgress) {
  return;
}
Enter fullscreen mode Exit fullscreen mode

Useful.

But that's UI throttling.

Not security rate limiting.

Again...

the attacker doesn't need your UI :')

They can directly automate requests against the endpoint.

This matters particularly for operations like:

  • Login attempts
  • Password resets
  • Expensive searches
  • Email sending
  • File processing
  • Administrative actions
  • Expensive third-party API calls

Without rate limiting, one endpoint can potentially become a resource exhaustion or abuse mechanism.

And authentication alone doesn't magically solve this.

Authenticated accounts can be:

  • Compromised
  • Automated
  • Buggy
  • Malicious

Prevention

Rate limiting belongs at a trusted server or infrastructure boundary.

Depending on the architecture, that could mean:

  • Application middleware/guards
  • API gateway
  • Reverse proxy
  • CDN/WAF
  • Distributed rate limiter

For distributed systems, you also need to think about whether counters are shared across instances.

Learn next

Study:

  • Rate limiting
  • Token bucket
  • Sliding-window algorithms
  • HTTP 429 Too Many Requests
  • NestJS throttling
  • Reverse proxies
  • Distributed rate limiting

11. Third-Party JavaScript Is Part of Your Security Boundary

Analytics.

Chat widgets.

A/B testing.

Payment SDKs.

Random npm packages.

Convenient scripts copied from somewhere at 2 AM because Stack Overflow said it works XD

Every script executing in your page deserves scrutiny.

If third-party JavaScript executes with access to your application's origin, you're trusting more than your own code.

That doesn't mean:

NEVER USE THIRD-PARTY JAVASCRIPT.

It means:

Know who you're trusting.

This is also why dependency security matters.

Your application can contain perfectly secure code...

and still inherit vulnerabilities through its software supply chain.

Prevention

  • Minimize unnecessary dependencies.
  • Keep dependencies updated deliberately.
  • Review high-risk dependencies.
  • Avoid casually loading remote scripts.
  • Use integrity/security mechanisms where appropriate.
  • Monitor dependency advisories.
  • Remove abandoned packages when feasible.

Learn next

Study:

  • Software supply-chain security
  • Dependency auditing
  • npm audit and its limitations
  • Subresource Integrity (SRI)
  • Content Security Policy
  • Lockfiles
  • OWASP Software Supply Chain guidance

12. Content Security Policy: Assume Something Eventually Goes Wrong

Imagine you've done everything correctly.

Then six months later someone introduces an XSS vulnerability.

Defense-in-depth asks:

Can we make exploitation harder even after the first defense fails?

That's where browser security mechanisms such as Content Security Policy (CSP) become interesting.

A well-designed CSP can restrict which scripts, styles, frames, and other resources the browser is allowed to execute or load.

It isn't:

CSP = true

XD

A weak policy can provide little protection.

A badly configured policy can break your application.

And CSP should not replace proper output handling or sanitization.

It's another layer.

Other important browser-side defenses include appropriate security headers and policies around framing, MIME handling, referrers, permissions, and transport.

Learn next

Study:

  • Content Security Policy
  • CSP nonces and hashes
  • Security headers
  • Clickjacking
  • frame-ancestors
  • HSTS
  • Referrer Policy
  • Permissions Policy

MDN and OWASP are excellent places to learn these properly.


So... How Do I Actually Think About Frontend Security?

After discovering all of this, it can become tempting to look at every line like:

IS THIS A VULNERABILITY??? :0

That's not particularly productive either XD

Instead, I've started using a simpler mental model.

Follow the data.

Whenever important data enters the system, ask:

1. Where did it originate?

User?

URL?

Database?

GitHub?

Third-party API?

Browser storage?

CMS?


2. Who controls it?

Us?

An administrator?

Any authenticated user?

Anyone on the internet?

Another service?


3. Where do we start trusting it?

Did the backend validate it?

Did we verify authorization?

Was HTML sanitized?

Are we merely assuming it's trustworthy because it came from our own API?


4. Where does it go?

HTML?

Database?

URL?

DOM?

Logs?

External API?

Another user?


5. What happens if an attacker controls it?

This is the fun question :')

Could they:

  • Execute JavaScript?
  • Modify another user's data?
  • Perform an admin operation?
  • Leak credentials?
  • Exhaust resources?
  • Inject misleading content?
  • Access something outside their permissions?

6. Where is the actual security boundary?

If your answer is:

"The button is hidden."

we need to talk XD


My Frontend Security Checklist

When reviewing frontend-heavy applications now, these are some of the questions I'd ask.

Authentication & Authorization

  • Are sensitive operations authorized on the backend?
  • Are sessions invalidated correctly?
  • Are credentials stored according to an explicit threat model?
  • Are admin endpoints independently protected?

Input & Output

  • Is untrusted input validated server-side?
  • Is dynamic HTML actually necessary?
  • Are framework security protections being bypassed?
  • Is dangerous output encoded or sanitized appropriately?

Browser

  • Are authentication cookies configured correctly?
  • Is there a sensible CSP?
  • Are sensitive values unnecessarily exposed to JavaScript?
  • Can the application be embedded where it shouldn't be?

API

  • Are expensive/sensitive endpoints rate-limited?
  • Does the backend assume the frontend already validated something?
  • Can users modify IDs or parameters to access resources belonging to someone else?
  • Are authorization checks performed against the requested resource?

Secrets

  • Does any real secret reach the frontend bundle?
  • Are external service credentials scoped to minimum required privileges?
  • Can exposed client keys be restricted by origin, API, quota, or permissions?

Dependencies

  • Do we actually need every dependency?
  • Are critical packages maintained?
  • Are third-party scripts necessary?
  • Are security advisories being monitored?

You don't need to memorize every vulnerability ever discovered.

You need to develop the habit of asking:

What assumption am I making about trust here?


What Should a Beginner Actually Study?

If you're reading this thinking:

Cool.

Everything is dangerous now.

Thanks :')

Don't randomly consume cybersecurity content for six months.

Start with web security fundamentals.

I'd learn roughly in this order:

1. HTTP fundamentals

Understand:

  • Requests and responses
  • Headers
  • Cookies
  • Origins
  • CORS
  • HTTPS

2. Authentication & authorization

Understand:

  • Sessions
  • Cookies
  • Tokens
  • RBAC
  • Server-side authorization

3. XSS

Learn:

  • Stored XSS
  • Reflected XSS
  • DOM-based XSS
  • Sources and sinks
  • Encoding vs sanitization

4. CSRF

Especially if you're using cookie-based authentication.

5. Input validation

Understand why both frontend and backend validation exist.

6. Browser security

Learn:

  • Same-Origin Policy
  • CORS
  • CSP
  • Secure cookies
  • Browser storage

7. API security

Learn:

  • Authorization
  • Rate limiting
  • Object-level access control
  • Validation
  • Abuse prevention

8. OWASP

Don't just memorize the OWASP Top 10.

Use it as a map of categories you should understand.

Two resources I'd keep returning to are:

OWASP Cheat Sheet Series

https://cheatsheetseries.owasp.org/

and:

MDN Web Security

https://developer.mozilla.org/en-US/docs/Web/Security

For practical exercises, PortSwigger's Web Security Academy is also extremely useful:

https://portswigger.net/web-security

Read something.

Break something in a legal training environment.

Understand why it broke.

Fix it.

That's much more effective than memorizing vulnerability names.


The Biggest Thing That Changed for Me

A while ago, when reviewing frontend code, I'd mostly ask:

Does this work?

Then:

Is this clean?

Is this maintainable?

Is this performant?

I'm slowly learning that there's another question that needs to exist alongside all of them:

What happens if somebody intentionally uses this in a way we didn't expect?

That's security thinking.

A form isn't only something a user fills out.

It's an input boundary.

An API response isn't automatically trusted data.

A hidden button isn't authorization.

A disabled button isn't protection.

An environment variable isn't automatically a secret.

A framework security escape hatch isn't something to casually bypass.

A successful request doesn't mean the request should have been allowed.

And a feature working exactly as intended doesn't mean it's safe.

That's probably the uncomfortable part of learning security.

Your code can be:

✓ Working

✓ Clean

✓ Tested

✓ Fast

✓ Beautiful

and still be:

✗ Please don't deploy this XD


Final Thought

I don't think becoming better at application security means memorizing every attack payload.

And I definitely don't think every frontend developer needs to become a penetration tester.

But if you're building applications that handle real users, real accounts, real data, or real permissions...

you need to understand trust.

Where it begins.

Where it ends.

And where you're accidentally creating it.

Because many security vulnerabilities aren't created by developers writing obviously dangerous code.

They're created by completely reasonable assumptions:

"The frontend already checked it."

"Only admins can see this button."

"It came from our API."

"It's inside .env."

"The framework handles security."

"The user can't send that value."

Individually, each sentence sounds harmless.

Until someone asks:

"...but what if that's not true?"

._.

That's the question I'm trying to get better at asking.

I'm still learning security.

Still finding assumptions in my own code.

Still reading documentation.

Still discovering browser behavior that makes me go:

"WAIT... it can do WHAT?!" :0

And probably still going to write something someday that a security engineer looks at and quietly closes their laptop XD

But now, when I read frontend code, I don't only see components, requests, forms, and state.

I see trust boundaries.

And once you start seeing those...

normal frontend code starts looking very different :))

Top comments (0)