<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shirajul Islam Shakur</title>
    <description>The latest articles on DEV Community by Shirajul Islam Shakur (@shakurshirajul).</description>
    <link>https://dev.to/shakurshirajul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137096%2Ffcc79370-6ace-4439-b7d5-db4e0c8b7cca.webp</url>
      <title>DEV Community: Shirajul Islam Shakur</title>
      <link>https://dev.to/shakurshirajul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shakurshirajul"/>
    <language>en</language>
    <item>
      <title>My Site Was Compromised in 48 Hours. I Thought It Was a CSS Bug.</title>
      <dc:creator>Shirajul Islam Shakur</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:55:19 +0000</pubDate>
      <link>https://dev.to/shakurshirajul/my-site-was-compromised-in-48-hours-i-thought-it-was-a-css-bug-1kig</link>
      <guid>https://dev.to/shakurshirajul/my-site-was-compromised-in-48-hours-i-thought-it-was-a-css-bug-1kig</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstlneo4ugpio8xqcwndo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstlneo4ugpio8xqcwndo.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I opened my laptop convinced I had a CSS bug. Shop links on my WooCommerce site were resolving to empty search URLs instead of product pages. Classic front-end problem a broken template path, probably something I had changed the night before.&lt;/p&gt;

&lt;p&gt;I debugged my own code for a while before I thought to check the user list. There was an admin account on the site that I had not created.&lt;/p&gt;

&lt;p&gt;This was a dummy store a throwaway project I had spun up to test a build, with no real customers and nothing of value on it. That detail turns out to matter, and not in the way I assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake
&lt;/h2&gt;

&lt;p&gt;My admin password was identical to my username. Four characters. I set it while getting the environment running and told myself I would change it before anyone found the site.&lt;/p&gt;

&lt;p&gt;Nobody found the site. Scanners found it. New WordPress installations get probed within hours of becoming reachable, and &lt;strong&gt;username-equals-password sits in the first few hundred guesses of any credential wordlist.&lt;/strong&gt; There was no window between "deployed" and "targeted" for me to fix it in.&lt;/p&gt;

&lt;p&gt;By hour 48 the access logs showed over 500 login attempts from roughly 20 IP addresses, deliberately spread thin so no single address tripped a rate limit. None of it was aimed at me specifically. It was a scanner working through a list, and my site was on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven seconds
&lt;/h2&gt;

&lt;p&gt;Two timestamps from the log, adjacent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01:51:15&lt;/strong&gt; - a login succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;01:51:22&lt;/strong&gt; - a second administrator account exists on the site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seven seconds. No human logs in, navigates to the users screen, fills out a form, and submits it in seven seconds. That was a script, and it was running the moment valid credentials were available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that actually taught me something
&lt;/h2&gt;

&lt;p&gt;The IP that logged in successfully appears only about twenty times in my entire log. It never brute-forced anything. It arrived already knowing the password.&lt;/p&gt;

&lt;p&gt;That is the part worth sitting with. One operation cracks credentials at scale; working logins get passed along to other operators. The machine that broke in was not the machine that guessed. &lt;strong&gt;My site had become a line item on somebody's list.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By that evening, three separate parties had logged in as administrator. The second one tried to create its own backdoor account and failed the first had already taken that step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a bug report is what saved me
&lt;/h2&gt;

&lt;p&gt;The intruders deactivated seven plugins, which is standard practice: anything that logs, scans, or blocks gets turned off first. WooCommerce happened to be among them.&lt;/p&gt;

&lt;p&gt;My theme wraps every WooCommerce call in a &lt;code&gt;function_exists()&lt;/code&gt; guard. I wrote those guards for robustness, so a missing plugin would not produce a fatal error on a live page. With WooCommerce deactivated, the theme did exactly what I designed it to do: instead of crashing, it degraded quietly. Shop links fell back to empty search URLs.&lt;/p&gt;

&lt;p&gt;That fallback is the only reason I noticed. &lt;strong&gt;Defensive code I wrote for reliability is what converted a silent compromise into a visible symptom.&lt;/strong&gt; Had the theme crashed outright I would have found it faster. Had it handled the absence perfectly, I might not have found it for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What contained the damage
&lt;/h2&gt;

&lt;p&gt;Administrator access to WordPress means arbitrary code execution the plugin editor alone is enough. So the question was never whether they could run code. It was what that code could reach.&lt;/p&gt;

&lt;p&gt;It ran as an unprivileged user inside a container. The database lived in a separate container with its port never exposed to the host. Getting from where they were to the host itself would have required a second, unrelated exploit.&lt;/p&gt;

&lt;p&gt;The result: container compromised, host clean. Recovery became &lt;em&gt;rebuild from a clean image and rotate every credential&lt;/em&gt; rather than &lt;em&gt;disinfect a server and hope you found everything.&lt;/em&gt; That distinction is the difference between an afternoon and a week.&lt;/p&gt;

&lt;p&gt;I want to be honest about this: I did not containerize for security. I did it because it made local development tidier. An architectural decision made months earlier for convenience is what turned a disaster into an inconvenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong about "pre-launch"
&lt;/h2&gt;

&lt;p&gt;The belief that did the damage was not really the weak password. It was the idea that a site nobody knows about is a site nobody will attack that "before launch" is a period during which normal rules are suspended.&lt;/p&gt;

&lt;p&gt;There is no such period. The moment a host resolves and a port answers, you are in production as far as the internet is concerned. Obscurity was never protecting me; I just had not been found yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline I should have had before deploying
&lt;/h2&gt;

&lt;p&gt;None of this is advanced. All of it takes less time than the recovery did.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A generated password, stored in a manager.&lt;/strong&gt; Not a memorable one. Not one you plan to change later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-factor authentication on every admin account&lt;/strong&gt;, from the first deploy rather than the first launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting on the login endpoint.&lt;/strong&gt; 500 attempts should never have been possible. Lock out after a handful of failures per IP and per username.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable the plugin and theme editors.&lt;/strong&gt; One line in &lt;code&gt;wp-config.php&lt;/code&gt;: &lt;code&gt;define('DISALLOW_FILE_EDIT', true);&lt;/code&gt; - it removes the most direct path from admin access to code execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block &lt;code&gt;xmlrpc.php&lt;/code&gt;&lt;/strong&gt; unless something you run genuinely needs it. It permits hundreds of credential guesses in a single request, which makes per-request rate limiting nearly useless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fail2ban, or equivalent&lt;/strong&gt;, watching the auth log and banning at the firewall rather than in PHP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access logging turned on and actually retained.&lt;/strong&gt; Without logs I would have had no idea how many parties were involved, or that credential resale had happened at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege in the runtime.&lt;/strong&gt; Unprivileged user, isolated database, no unnecessary ports. This is the layer that worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A forensic detail worth knowing
&lt;/h2&gt;

&lt;p&gt;WordPress does not record an IP address when a user account is created. It does store the exact registration timestamp.&lt;/p&gt;

&lt;p&gt;Take that timestamp, search your web server access logs for the matching second, and the request that created the account is sitting there with its source IP attached. That one join is how I established the sequence, who got in first, who arrived later, and which address had never attempted a password at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually took away
&lt;/h2&gt;

&lt;p&gt;My authentication layer failed completely. Not partially completely. The thing that limited the damage was a layer I had put in place for entirely unrelated reasons.&lt;/p&gt;

&lt;p&gt;That is the argument for defense in depth, and I did not really understand it until one layer failed and a different one held. You are not building a wall. You are arranging things so that the failure of any single control is survivable, because eventually one of them will fail most likely the one you were confident about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.shakurshirajul.com/blogs/wordpress-compromised-in-48-hours" rel="noopener noreferrer"&gt;shakurshirajul.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>What Shipping a WordPress CRM Taught Me About Being a Developer</title>
      <dc:creator>Shirajul Islam Shakur</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:29:04 +0000</pubDate>
      <link>https://dev.to/shakurshirajul/what-shipping-a-wordpress-crm-taught-me-about-being-a-developer-17po</link>
      <guid>https://dev.to/shakurshirajul/what-shipping-a-wordpress-crm-taught-me-about-being-a-developer-17po</guid>
      <description>&lt;p&gt;Most advice about becoming a developer stops at the moment you get hired. That is the part I found least useful. The gap that actually mattered for me was the one between finishing tutorials and shipping software other people depend on — and nothing I had read prepared me for what that gap contained.&lt;/p&gt;

&lt;p&gt;This is what I learned crossing it, building &lt;a href="https://www.shakurshirajul.com/projects/bit-crm" rel="noopener noreferrer"&gt;Bit CRM&lt;/a&gt;, a sales CRM plugin published on the WordPress.org directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it started
&lt;/h2&gt;

&lt;p&gt;My father brought a desktop computer home for work when I was three or four. It came with Road Rash, Virtua Cop, and Need for Speed II. I played them constantly, but the machine itself held my attention longer than the games did. I watched what my father did and copied it — media players, Microsoft Word, Excel learning entirely by trial and error. That habit of poking at something unfamiliar until it makes sense is the one thing that has carried through every stage since.&lt;/p&gt;

&lt;p&gt;Studying Computer Science and Engineering at IIUC gave that curiosity structure. I learned C and C++, then spent a long stretch on competitive programming on Codeforces and Beecrowd. I want to be precise about what that did and did not teach me, because it is commonly oversold: it did not teach me to build software. It taught me to sit with a problem I could not immediately solve without becoming frustrated, and to break it into pieces small enough to reason about. In production work, that turns out to matter more than knowing any particular algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part tutorials do not cover
&lt;/h2&gt;

&lt;p&gt;I learned HTML, CSS, JavaScript, React, then Node, Express, and MongoDB — roughly the standard path. Every tutorial builds its app on an empty page, in an environment that belongs entirely to you.&lt;/p&gt;

&lt;p&gt;Real work is almost never like that. When I joined Bit Code and started on Bit CRM, the job was to build a React application &lt;em&gt;inside the WordPress admin&lt;/em&gt; — a host environment nobody on my team controlled. WordPress ships its own global styles. Other plugins enqueue their own scripts. The admin shell assumes it owns the page. None of that is a bug; it is simply the environment, and your application is the guest.&lt;/p&gt;

&lt;p&gt;That reframes the work. You stop asking "what is the cleanest way to build this?" and start asking "what is the cleanest way to build this &lt;em&gt;here&lt;/em&gt;?" Those are different questions, and only the second one ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting into conventions you did not choose
&lt;/h2&gt;

&lt;p&gt;Bit CRM integrates with Bit Form, Bit Flows, and Bit Integrations — plugins that were already shipping and already had users before I wrote a line. That meant the CRM had to match the data flow those plugins established rather than impose its own.&lt;/p&gt;

&lt;p&gt;My instinct as a newer developer was to treat that as a constraint to work around. It is not. A convention that already works, that other engineers already understand, and that existing users already depend on carries real value and the cost of breaking it is paid by people who never agreed to the change. Learning to read an existing system carefully enough to extend it, rather than rebuilding it in the shape I would have chosen, was probably the largest single shift in how I work.&lt;/p&gt;

&lt;h2&gt;
  
  
  State is where complexity actually accumulates
&lt;/h2&gt;

&lt;p&gt;I built the plugin dashboard, the client portal, the invoice module, the calendar view, and the activity system covering tasks, meetings, and calls. Individually, none of those is difficult. The difficulty is what happens when you have all of them at once.&lt;/p&gt;

&lt;p&gt;What kept it manageable was a strict separation: &lt;strong&gt;server state in TanStack Query, local UI state in Zustand.&lt;/strong&gt; Anything that lives in the database deals, invoices, contacts, activities — is owned by TanStack Query, which handles caching, refetching, and staleness. Anything that exists only in the browser which modal is open, which filter is applied, what is typed into a form — lives in Zustand.&lt;/p&gt;

&lt;p&gt;The rule sounds obvious written down. It is easy to violate when you are moving fast and it would be convenient to copy a server value into local state "just for now." Every time that happens you have created two sources of truth that will eventually disagree, and the bug it produces will surface somewhere unrelated, weeks later. Holding the line on that separation is what let the module count grow without the interface becoming unpredictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building for something other than a human
&lt;/h2&gt;

&lt;p&gt;The most unusual part of the project was contributing to the MCP server and the built-in AI assistant, which let the CRM be driven from tools like Claude and ChatGPT.&lt;/p&gt;

&lt;p&gt;Designing an interface for a model is genuinely different from designing one for a person. A person tolerates ambiguity — they read a label, look at surrounding context, and infer what you meant. A model has the tool definition and nothing else. Names have to say exactly what a thing does, arguments have to be unambiguous, and errors have to explain what went wrong in terms of what to do next, because there is no user to intuit around a bad message. It made me a more careful API designer than any amount of front-end work had.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself two years ago
&lt;/h2&gt;

&lt;p&gt;Build things that have users. Not portfolio pieces — things where someone is inconvenienced if you get it wrong. Everything that has actually made me better as a developer came from that pressure: caring about maintainability because I will be the one maintaining it, caring about deployment because a broken release is visible, caring about clear naming because someone else has to read it.&lt;/p&gt;

&lt;p&gt;You cannot simulate that with a tutorial project, and you do not need to be senior to get access to it. You need one real thing, and then the patience to keep working on it after the interesting part is over. That is where the learning is.&lt;/p&gt;

&lt;p&gt;The tools I use now — &lt;strong&gt;React, Next.js, TypeScript, Node, Laravel, MongoDB, MySQL&lt;/strong&gt; — are less important than that, and they will change. Some of them already have. You can see what I am building on my &lt;a href="https://www.shakurshirajul.com/projects" rel="noopener noreferrer"&gt;projects page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.shakurshirajul.com/blogs/my-full-stack-developer-journey" rel="noopener noreferrer"&gt;shakurshirajul.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>wordpress</category>
      <category>career</category>
      <category>react</category>
    </item>
  </channel>
</rss>
