by Ethan Ryan

How user feedback improved my app’s security

XCz27d4cvhkcmEoIF9U6cliByYQARlkkRM59
security photo via Pixabay

Getting published on freeCodeCamp’s Medium publication was super exciting.

The week my post was accepted, I was busy with work and headed out of town for the weekend, so I didn’t get a chance to check Medium for a few days. I’d gotten some email notifications, and was excited to catch up on the responses to my piece when I got a chance.

v0ypDXKM2V2tnU70O6qrejSXM9DEd0b3UhWF
Medium notifications

Nice! That big green circle meant claps! New followers! People reading my words and checking out my story generator app! This was awesome!

Then I read the messages.

7JVfMXKsZ6pCZ7TGtm-h1-5dJEUQemMqBpL0
comment one

Uh-oh spaghetti-os.

lrcyUuw6DTmxBiTOr7jMrcnBKOHm6ExjIRHE
comment two

No bueno.

a-ofyiUOaAunD3bHVbuYm9bzinFfvHzodIBw
comment 3

Hmm, makes sense.

RFAm14hs1xKedDqRWmGVCEcU2sTq0MPrvUHZ
comment 4

Yikes!

Truth be told, I’d never checked out the Network tab in Chrome’s developer tools ¯\_(ツ)_/¯.

I spend tons of time in the console of the browser, reading my logs and warnings and errors, but not much time with the other Developer Tools options.

Those comments were super helpful, and made me realize I had work to do.

To summarize thus far:

  • Good news: WordNerds was getting some new users! :)
  • Bad news: Bad guys could still see a list of all my users, and their email addresses :/

All anyone had to do to find all my users’s email addresses was go to wordnerds.co, open the console, click on Networking, and go to: https://word-nerds-api.herokuapp.com/users

They’d see this:

01-PJrx5GmmzwYfkteS8-BY9vOgM5Bl0b7fx
WordNerd’s /users API endpoint
Note: My first few users didn’t have email addresses stored in the database because they signed up for WordNerds before I made email addresses required attributes via frontend authentication.

Scrolling through that API endpoint, I also noticed another problem that needed to be fixed:

MNjse84v41BnzDGf-j4zifMguGqc7K4lvDTf
lorem ipsum username

Oops. My username attribute didn’t have any string length limit. Or if it did, that limit was too damn high. Nobody’s username needs to be that long.

For example:

bLLS-ev9iBj7Puum5QGlvMUXEEb1DPwl0oAw
Navy Seal Copypasta username, profanity blurred

Jeez Louise. How would anyone remember to paste in the Navy Seal Copypasta in order to sign into WordNerds?!

What a hassle. I didn’t want to give my users a bad user experience, expecting them to remember to copy and paste all that copypasta.

Username input fields are like children: they crave limits.

So I had some work to do.

  1. I had to protect my user’s email addresses. Again. I thought I’d fixed that last time, but boy was I wrong.
  2. I had to limit how many characters a username could be.
  3. As my helpful commenters had pointed out, I should only be retrieving the absolutely necessary data from the backend from each API endpoint. I was returning too much data, which was bad for both security and performance reasons. I had to protect my user data and limit the amount of data being returned for each API call.

Cool cool cool. Work work work. Time to get to work.

Protecting user data

My first and most pressing issue: making sure I wasn’t logging all my users names and email addresses to my /users API endpoint as JSON.

There were multiple ways to fix this, and after some thought I decide upon the most obvious, easiest approach, so obvious and easy I was surprised I didn’t realize it sooner. I had no need at all for an API endpoint for all users. So I could simply delete that API call from the frontend, and the corresponding Rails method on the backend.

I did like showing the total number of users in my app’s Metadata component, though. It was just a simple number, but I liked watching it slowly grow in size as more people signed up on my site.

So I decided to keep that number, and eliminate all that user data showing up on the API endpoint.

I kept the API call exactly the same on the frontend, and on the Ruby on Rails backend, I changed the index method in the UserController from this:

def index   users = User.all   render json: usersend

to this:

def index   users = User.all.size   render json: usersend
Note: I could have used length or count instead of size, but size is the best bet according to this StackOverflow post.

Now instead of returning an array full of user objects, containing usernames and email addresses, my backend is instead simply returning a number.

BEFORE:

pgjW8cW1-EJkFlQ9Sitdmltrf-nbaaJ55L-f
/users API endpoint — BEFORE

AFTER:

CSVl2iG9jXMEA9KXbD2Hl-sXlWnNVAdKO8Fk
/users API endpoint — AFTER

Whoa! What an amazing transformation!

After that change to the backend, I had some minor changes to the frontend. Instead of rendering props.users.length in my Metadata component, I could simply render props.users. And I could change that name in the container state from this.state.users to this.state.userCount. Easy updates.

No more user data in my publicly accessible API endpoint!

Well, my usernames and email addresses where still accessibly via the /stories endpoint, so I still had that to fix. But that could be dealt with soon.

Limiting username length

I didn’t like seeing that a username could be as long as the Navy Seal Copypasta, and although it’s nutso that someone would even try making their name that long, I’m glad they did, because now I could fix that issue!

Thank you, whoever made your WordNerds username crazy long. I’m looking at you, Lorem Ipsum and Navy Seal Copypasta.

I already had some validations on my frontend to make sure that users logging in or signing up for WordNerds had usernames and passwords that were not empty.

My SignUpForm is a stateful component that called validate in my render function, as well as in my canBeSubmitted function.

I got that validate function from this freeCodeCamp blog post, probably about a year ago.

My original validate function looked like this:

validate(name, password) {   return {      name: name.length === 0, //true if username is empty      password: password.length === 0 //true if password is empty   }}

I decided to refactored this function, making it less succinct, but also more clear, so current and future me will understand it:

validateFormInputs(name, password) {   let nameIsInvalid = (name.length === 0) //true if empty   let passwordIsInvalid = (password.length === 0) //true if empty   let errorObject = {      name: nameIsInvalid,      password: passwordIsInvalid   }   return errorObject}

I can hear some of you groaning, “Ugg, you made that succinct function so long and ugly! You added variable names for no reason!”

Sure, I’m adding some lines here, but to me, I can now understand more quickly what is happening in this function.

Now I simply add some more conditions to be met. Apart of a valid username not being empty, I’m also validating that it cannot be longer than 15 characters.

I choose the number 15 because that’s what Twitter allows for its usernames, and if it’s good enough for Twitter, it’s good enough for WordNerds.

With my new condition for usernames, my function looks like this:

validateFormInputs(name, password) {   let nameIsInvalid = (name.length < 2 || name.length > 15)   let passwordIsInvalid = (password.length === 0)   let errorObject = {      name: nameIsInvalid,      password: passwordIsInvalid   }   return errorObject}

Nice! Now the Navy Seal Copypasta can now longer be used as a username on WordNerds.

Sorry copypasta fans! Gotta keep your usernames at or under 15 characters from here on in.

TA0dzPY0YYqljqaX3ATdn0Wg4niTczfDqEh6
invalid name if more than 15 characters

I realized it was good practice to not allow spaces in usernames either. “Bob Smith” would be a bad username, as would “ ”. I considered adding a simple regex to my function, when I learned about the input pattern attribute in HTML5. Cool! No need to add anything to my function, I could simply update my JSX form field for the username.

My React frontend’s username form field now looks this:

ofzGmtUNes-oQwFIrGsH2fuEzujDUJMTLABj
LoginForm username form field

Which results in this alert in the browser:

tVyjswWUpBxtn15mInmg1jAh7lWKCPtHDi6y
bad name alert

I made similar updates to my SignUpForm as I did to my LoginForm, and included some validations for email addresses.

Sweet, now I just had to make sure there weren’t any email addresses being made visible in my /stories API endpoint. To the backend!

Limiting data returned for each API call

Blah blah blah, a bunch of stuff on the backend.

I didn’t do a good job or writing this stuff down cuz I was trying to get it done quickly, and when that failed, I was trying to get it done.

I’m continuing to think of ways to improve what data is returned from my API endpoints, to make my app both more secure and more scalable.

But to summarize, no more email addresses being made visible in my /stories API endpoint!

Now each story has a user_name attribute, in addition to a user_id attribute, but no more email addresses are accessible via the API.

The argument could be made that I’m still exposing my app’s users’ usernames, and that I shouldn’t be doing that. But I’m treating those usernames as public info. Users can choose their usernames, so it’s up to them how revealing they want to be in their username choice. It could be RichAt123FakeSt, or it could be batman6669. Who am I to judge what my app’s users choose as their usernames? It’s not like I’m revealing their extremely personal email addresses or anything! I mean, not anymore.

Conclusion: Feedback is good

After making those security updates, I made a few more fun changes as well. It’s fun to continually be improving my app, thanks to helpful feedback from internet strangers, as well as whatever zany feature I think will make it better.

Check out WordNerds here, at WordNerds.co.

Thanks for reading, nerds!

Till next time.