Cyber Town; training data next 100 miles

ICTWEB430 Produce server-side script for dynamic web pages

ICTWEB43060 nominal hoursIn progressUpdated 20 September 2026

The unit as writtenunit scope

This is the official scope of the unit, kept here (folded) so its intended coverage is visible at a glance and my own notes can be placed against it. The notes below are mine; they follow this scope where it still holds and go past it where current practice has moved on. That happens more in this unit than in most, because the unit is written around three specific technical choices that have each been superseded: XHTML as the markup standard, direct query-building against a database, and a password hashing approach that current guidance would treat as a defect rather than as dated.

Unit: ICTWEB430 Produce server-side script for dynamic web pages. A national ICT unit in the web sector, used here as an elective in the Certificate IV study. Release 1, first released with ICT Information and Communications Technology Training Package Version 4.0. It supersedes and is equivalent to ICTWEB415. No prerequisites, and no licensing, legislative or certification requirements apply at the time of publication. Nominal hours are 60, from the Victorian purchasing guide for the ICT training package.

What the unit describes: the skills and knowledge required to produce server-side scripts for dynamic web pages using a range of relevant features from different languages. It applies to people working as web designers who apply a wide range of knowledge and skills across different ICT environments to support organisations requiring broad ICT support.

The five elements and their performance criteria, as written:

Analyse the requirements for web documents requiring server-side dynamic interaction. Review and assess user requirements with the user to determine the necessary web document; determine and document the dynamic functionality of the web document to meet user requirements; determine the appropriate language based on the required dynamic functionality; and finalise the web document requirements.

Design server-side scripts. Design the web document and server-side code to interact with an external data source; design the web document and server-side code to allow an administrator to insert, update and delete entries in the external data source; and implement security features in the web document based on user requirements.

Produce the web documents. Write extensible hypertext markup language (XHTML) in line with XHTML standards to define web page structure; write server-side scripts in line with XHTML standards to enable customised page content generation; and upload images to a web-hosted database to enable dynamic retrieval for the web page.

Test the scripts and debug. Test the web document and rectify issues to meet user requirements; and complete test results documentation and submit it to a superior for discussion and acceptance.

Set up security. Determine and implement permissions to prevent error messages displaying to the public; and configure server software to minimise potential database attacks.

Performance evidence: create a dynamic web page according to one set of user requirements; use server-side scripting to retrieve information from a web-hosted database in two different instances; analyse three language options with their advantages and disadvantages to select the most appropriate language; configure one web server to deliver the website using HTTPS; create one script for each of inserting, updating and deleting data from a web server database, implementing security features, uploading and retrieving images, and managing sessions and creating secure logins; and test the web document and take corrective action to meet requirements.

Knowledge evidence: server-side technologies and at least three web scripting languages with their advantages and disadvantages; server-side web analysis and design parameters; XHTML standards; testing tools and processes with their advantages and disadvantages; control structures and object-oriented programming; web-programming concepts including the hypertext transfer protocol and HTTP Secure, stateless programming, session management, authentication and web security, and database vulnerabilities with preventative software configuration and programming practices; and organisational procedures to document test results.

Foundation skills: reading, to identify, analyse and evaluate workplace instructions and technical documentation; writing, to develop clear and well-organised material for a specific audience using precise language; oral communication, to articulate requirements in language appropriate to the audience; navigating the world of work, to identify and comply with current standards; interacting with others, to build rapport with clients and colleagues; and getting the work done, covering planning and sequencing own work, routine decision-making, and using digital technologies with an awareness of data security and safety.

Assessment conditions: the skills must be demonstrated in a workplace or simulated environment with conditions typical of an ICT working environment, with access to a web-hosted database, a server with software for configuration, user requirements relating to web documents, a software development environment, XHTML standards, and individual users as well as a superior in the organisation.

Source: the application, elements, performance criteria and foundation skills are the training.gov.au unit descriptor for ICTWEB430 (Release 1, ICT Training Package Version 4.0); the performance evidence, knowledge evidence and assessment conditions are from the companion assessment requirements document. Both were read from the copies held in the unit folder in the vault. Nominal hours are from the Victorian purchasing guide for the ICT training package.

A note on the scope itself. Two things the unit names are worth flagging before the notes begin, because everything else depends on which version of them you learn. "XHTML standards" appears three times in the performance criteria and once in the assessment conditions, and XHTML has not been the current markup standard since the early 2010s; there is a section below on exactly what happened and what survives. And "authentication and web security" in the knowledge evidence covers ground where the specific practice taught in the delivered material is no longer merely dated. These notes teach the current answer and say plainly what it replaced, because the difference is the point.

What "server-side" actually means, and why it is the security boundary

A web page that does anything useful is a program split across two machines. Part of it runs on a server the organisation controls; part of it runs in a browser on a stranger's device, where it can be read, modified, or replaced entirely. Every sentence in this unit follows from that split, so it is worth getting exactly right at the start.

Client-side code, meaning the HTML, CSS and JavaScript delivered to the browser, is visible to the user. They can read it, change it, run it in a modified form, or ignore it entirely and send whatever request they like using a tool that is not a browser. Server-side code is not visible. The user sees the output, never the source. That is why the server-side is where anything that must be true gets decided: what is in the database, who is allowed to see it, whether a password is correct, whether a price is what the page said it was.

The consequence follows in one line and is the most useful thing on this page: client-side validation is a convenience, server-side validation is the control. Checking in JavaScript that an email address contains an at sign gives the user a fast, pleasant correction without a round trip. It provides no security whatsoever, because the attacker simply does not run your JavaScript. Every check that matters is repeated on the server, on the assumption that everything arriving from the browser is hostile: form fields, URL parameters, headers, cookies, uploaded files, and the order and number of requests.

The unit's elements track the shape of that work. Requirements analysis decides what dynamic behaviour is needed. Design decides how the server talks to the data source, and what security features the requirements demand. Production writes the markup and the server-side code that generates it. Testing proves it behaves. Setting up security hardens the server that runs it. What follows takes those in order, with the current version of each.

sequenceDiagram
  participant B as Browser (untrusted)
  participant S as Web server
  participant A as Server-side script
  participant D as Database
  B->>S: HTTPS request (URL, headers, cookies, form data)
  S->>A: hand off to the interpreter
  A->>A: validate and authorise, every time
  A->>D: parameterised query
  D-->>A: result set
  A-->>S: generated HTML, escaped on output
  S-->>B: HTTPS response
  Note over B,A: nothing the browser sends can be trusted
Static and dynamic, and what "dynamic" now includes

A static page is a file on disk. The server finds it and sends it, unchanged, to everyone. A dynamic page is generated by a program each time it is requested, so its content can depend on who is asking, what is in the database, what time it is, or what the user just submitted. That distinction is the reason this unit exists.

The classic pattern is server-side rendering: the script queries the data, builds the complete HTML, and sends a finished page. The browser receives markup and displays it. That is what the unit teaches and it is still, in 2026, the right default for content-driven sites. The alternative that grew up alongside it sends a mostly empty page plus a JavaScript application, which then fetches data from an API and builds the page in the browser. Between the two sits the pattern most new work uses now: render on the server for the first view, so the page arrives fast and is readable without JavaScript, then update parts of it in the browser as the user interacts. The names change (server-side rendering, hydration, islands, progressive enhancement), the idea does not, and the swing back towards rendering on the server after a decade of browser-side applications is one of the more interesting reversals in the field.

Two practical reasons to care, beyond fashion. Search engines and link previews read what the server sends. And a page that renders on the server works on a slow connection and an old device, which in the Northern Territory is not a hypothetical.

XHTML, and what actually happened to it

The unit requires markup written "in line with XHTML standards". This needs addressing directly, because it is the clearest example on the site of a specification that a training package froze and the world moved past.

The history, briefly. HTML was always forgiving: unclosed tags, mixed case, attributes without quotes, and browsers that guessed at what was meant. XHTML was an attempt to fix that by reformulating HTML as XML, which is strict. XHTML 1.0 became a W3C Recommendation on 26 January 2000, with a second edition on 1 August 2002, and XHTML 1.1 followed on 23 November 2010. The rules were the point: every element closed, empty elements self-closed as <br />, all tags and attributes lowercase, all attribute values quoted, and a document type declaration at the top. In principle a parse error meant the document failed rather than being guessed at.

Then it stopped. The W3C let the XHTML2 working group's charter expire at the end of 2009 and did not renew it, saying it would not revise XHTML 1.0 or allocate further resources to XHTML 1.1. The energy had gone to a rival effort, the Web Hypertext Application Technology Working Group, which was developing HTML5 as a specification that described what browsers actually did rather than what they ought to do. HTML5 won, decisively, because it gave authors new elements and APIs they wanted and did not require them to accept a failure mode nobody liked.

The two specifications then merged. For years there were two competing HTML specifications, the W3C's and WHATWG's, which the W3C itself described as "generally harmful for the community". On 28 May 2019 the two bodies signed an agreement to work together on HTML and the DOM in the WHATWG repositories, producing a Living Standard with W3C Review Draft snapshots. On 28 January 2021 the W3C endorsed the WHATWG Review Draft as its HTML Recommendation and republished its own HTML5.x Recommendations as superseded. The XHTML 1.0 and 1.1 Recommendations had already been marked "Superseded Recommendation" on 27 March 2018, with the standard wording directing readers to follow the latest HTML specification.

So what is the current standard? The HTML Living Standard, maintained by WHATWG. It has no version number, because it is continuously revised; there will be no HTML6. A current document opens with <!DOCTYPE html>, five characters of ceremony instead of a paragraph of it, and that declaration exists only to keep browsers out of quirks mode.

What of XHTML actually survives, and is worth keeping as a habit. The XML-serialised form still exists and is still used where a document has to be machine-parsed strictly: SVG and MathML are XML vocabularies embedded in HTML, and self-closing syntax is meaningful inside those subtrees; EPUB content documents are XHTML or SVG. In ordinary HTML, <br /> parses fine and means nothing extra, and tag case is not enforced. But the discipline XHTML taught, close what you open, quote your attributes, nest correctly, keep it lowercase, produces cleaner markup and fewer surprises, and every modern formatter and linter enforces most of it anyway. Teach the habits; do not teach the doctype.

The practical position for this unit is therefore straightforward. Where a performance criterion says "XHTML standards", write valid, well-formed HTML to the Living Standard, using semantic elements (<header>, <nav>, <main>, <article>, <section>, <footer>) rather than a wall of <div>, validate it, and know why the phrase in the unit says something else. The benefits of the move that the delivered material asks you to list are real and worth knowing: a simpler doctype, a parser that recovers from author errors rather than failing, native elements for video, audio, canvas and form input types, APIs that XHTML never had, and far better behaviour on mobile browsers.

Choosing a server-side language

Performance criterion 1.3 asks you to determine the appropriate language based on the required dynamic functionality, and the performance evidence asks you to analyse three options with their advantages and disadvantages. This is an analysis exercise rather than a preference, so it is worth knowing the current field rather than the field as it stood a decade ago.

PHP is the language this unit is almost certainly taught in, and there is a good reason for that beyond inertia: it runs on essentially all shared hosting, it is free, it is easy to start in, and an enormous proportion of the existing web runs on it. W3Techs, surveying detectable technologies on high-traffic public websites, put PHP at 69.9 per cent of sites with a known server-side language in its survey dated 13 September 2026. Its weaknesses are real too: a long history of inconsistent standard library naming that the language cannot shed, and a reputation earned in the PHP 4 and 5 years that the modern language has largely outgrown. PHP 8 is a genuinely different language from the one most criticism was aimed at, with typed properties, union types, constructor promotion, match expressions, attributes, and a much stronger tendency to throw exceptions where older versions emitted a warning and carried on.

JavaScript and TypeScript on Node.js is the common choice where the same team writes the front end, because it is one language across both halves. TypeScript rather than plain JavaScript in nearly all new work of any size. Strong for APIs and for anything event-driven; the ecosystem is vast and moves quickly, which is both the attraction and the cost.

Python, with Django for full applications, FastAPI for APIs and Flask for small services, is strongest where the web application is a front door onto data, analysis or machine learning work. Readable, well-supported, and the obvious choice when the rest of the system is already Python.

C# on .NET is the standard in Microsoft-aligned corporates and government, including much of the Australian public sector. Strongly typed, excellent tooling, and the natural answer where the organisation already runs on Microsoft infrastructure.

Java with Spring Boot is entrenched in banking, insurance, telecommunications and other large enterprise back ends, where it is rarely displaced once in place.

Go is used for infrastructure, network services and high-throughput APIs more than for page-rendering applications. Ruby on Rails holds a smaller but stable niche in startups and long-lived software products. Rust appears in performance-critical or security-critical components rather than as a whole web application, growing from a small base.

Perl deserves a sentence only because older material lists it. It is essentially finished as a choice for new web work: W3Techs puts it at 0.1 per cent of sites, and the Stack Overflow Developer Survey 2025 shows 3.8 per cent of respondents using it at all. It survives in legacy CGI scripts and in systems administration. If a comparison list still offers Perl as a live option alongside PHP and Python, that list is historical.

How to actually make the choice. The honest criteria are rarely about the language's features:

What decides the language in practice

What the host supports. Shared cPanel hosting runs PHP. If that is the deployment target, the decision is largely made.

What the team already knows. A team fluent in one language will ship a better application in it than a marginally better-suited language they have to learn.

What the application does. Heavy data or model work points at Python. A shared codebase with a JavaScript front end points at Node. An existing Microsoft estate points at .NET.

What it has to integrate with. The language with the maintained client library for the systems you must talk to has a real advantage.

How long it has to live, and who will maintain it. A five-year application in a small organisation wants boring, widely known technology.

Licensing and cost. All the mainstream options are free; what costs money is hosting, and that varies more than the language does.

One caution on evidence, because it is a good general lesson. W3Techs says PHP runs most of the web; the Stack Overflow Developer Survey 2025 says JavaScript is used by 66 per cent of respondents, Python by 57.9 per cent and PHP by 18.9 per cent. Both are true, because they measure different things. W3Techs counts public websites, where one WordPress installation weighs the same as a large bespoke application, and detection is inferential. Stack Overflow counts self-reported use by people who chose to answer a survey, which over-represents newer tooling and under-represents enterprise Java and .NET shops. Quote either, name which it is, and never treat one as the other.

Keeping PHP current, since it is the language on the ground

If the work is in PHP, knowing which versions are supported is part of professional practice rather than trivia, because an unsupported interpreter is an unpatched one.

As at September 2026 four branches are supported: 8.2, released 8 December 2022, in security support until 31 December 2026; 8.3, released 23 November 2023, security support until 31 December 2027; 8.4, released 21 November 2024, in active support until the end of 2026 and security support until the end of 2028; and 8.5, released 20 November 2025, active until the end of 2027 and security until the end of 2029. The policy is two years of active bug-fix support followed by two years of security fixes, four years in all; php.net keeps the current position on its supported versions page.

Everything before that is finished. PHP 7.4, the last of the 7.x line, reached end of life on 28 November 2022, and PHP 8.0 on 26 November 2023; PHP 8.1 ended on 31 December 2025. So any tutorial, deck or hosting account still on PHP 7 is running an interpreter that has received no security fixes for nearly four years. This is the first thing to check on any site you inherit, and a shared hosting control panel usually lets you change it in one menu.

The PHP 8 changes that bear on this unit are mostly about error handling. A great many conditions that once emitted a warning and returned false now throw an exception, which means code written for PHP 5 or 7 can fail loudly on a modern interpreter in places it used to limp along. password_hash(), for instance, now throws a ValueError on an invalid algorithm rather than returning false. That change is an improvement, because silent failure in authentication code is exactly the failure you least want, but it does mean migrating older code is a real job rather than a version bump.

Analysing requirements, and writing them down

The first element is requirements work, and the discipline here is the same as in any other client-facing unit: the client describes an outcome, and you have to turn it into a specification precise enough to build and test against.

What "dynamic functionality" means in a requirements conversation is a set of specific behaviours, each of which implies data and a page. A client saying "we want to be able to update the site ourselves" is describing an administrative interface with authentication, a list of content items, and create, read, update and delete operations on each. A client saying "customers should be able to see their orders" is describing user accounts, a session, a query filtered by the logged-in user, and an authorisation check that stops one customer seeing another's. Writing those out as numbered behaviours, each with the data it needs and the rules that govern it, is the deliverable performance criterion 1.2 asks for.

Three questions to ask that change the design most:

Who is allowed to do each thing? This is the question that produces the privilege model, and getting it explicit at requirements time is the difference between an authorisation system and a set of patches. A typical content system has a public reader who need not log in, an ordinary user who can edit only their own details, a content editor who can create and change pages, an administrator who can manage users below their own level, and a top-level administrator who can do anything including managing other administrators. Write that as a grid of roles against actions, because that grid becomes both the code and the test plan.

What happens when it goes wrong? Requirements usually describe the success path. The failure paths are where most of the code goes: duplicate email address on registration, wrong password, expired session, file too large, database unreachable, a user who edits something another user just deleted.

What has to be kept, and for how long? This decides the data model and it decides the privacy position. A site that stores customer records is handling personal information, and the obligations that come with that belong in the requirements conversation rather than being discovered afterwards.

The requirements get finalised and agreed, which is performance criterion 1.4, and that agreed document is what the testing in the fourth element is run against.

Designing the data layer

The second element is design, starting with how the web document and the server-side code interact with an external data source. In a small application that source is a relational database, usually MySQL or MariaDB on shared hosting, or PostgreSQL where the choice is free.

The entity relationship model comes first: what things does the application store, what attributes does each have, and how do they relate? A content management system has users and pages, each page authored by a user, each user holding a role. Drawing that before writing a table is worth the ten minutes, because a data model is the part of an application that is most expensive to change later.

Normalisation is the principle that each fact is stored in one place. If a user's name appears in the users table and again in every page row, the two will eventually disagree. Store the user once, reference them from the page by a foreign key, and join when you need both. The small application exception worth knowing is that deliberate denormalisation for read performance is a legitimate decision at scale; it is a decision, not an accident.

Choosing the column types is where a lot of quiet damage happens. A primary key that auto-increments and does not allow duplicates. Email as a unique column, sized generously. Text columns sized for the real world rather than the optimistic case. Dates and times stored as date and time types rather than as strings, in UTC, and converted for display. Booleans stored as booleans rather than as a single character, which is a pattern you will meet in older schemas. And the one that catches people: a password column sized for what the hashing function actually returns plus room for the algorithm to change, which means at least 255 characters, not 60.

The operations are the four the unit names: insert, update, delete, and the reads that underpin everything. A deliberate design point is what "delete" means. Hard deletion removes the row, which is simple and irreversible. Soft deletion sets an inactive flag and filters it out of queries, which keeps the history, keeps referential integrity, and lets an administrator restore something deleted by mistake. Most administrative systems want soft deletion for content and users, with true deletion reserved for a top privilege level and for genuine erasure requests.

Talking to the database without creating a vulnerability

This is the technical heart of the unit, and it is the place where the delivered practice and current practice diverge most sharply.

The vulnerability first. SQL injection happens when data supplied by a user is placed into a query as though it were part of the query's code. If a login page builds its query by pasting the submitted email straight into a string, then an input of ' OR '1'='1 turns a condition that should match one row into one that matches every row. More capable inputs read other tables, modify data, or on a badly configured server reach the operating system. The forms worth recognising are error-based, where the application returns database errors that leak the schema; union-based, where SQL's UNION appends attacker-chosen rows to the result; and blind, where nothing is displayed and the attacker infers data one true-or-false question at a time. SQL injection has been on every OWASP Top 10 since the list began.

The fix is not escaping. Escaping user input, which is what mysql_real_escape_string() and its successors do, is listed last among the defences in OWASP's SQL Injection Prevention Cheat Sheet and is explicitly described as strongly discouraged and fragile compared with the alternatives. The reason is that escaping is a filter, and filters have edge cases: character set mismatches, numeric contexts where quotes are not used, and contexts the escaping function was never designed for.

The fix is parameterised queries, also called prepared statements. The query structure and the user's data are sent to the database separately, so the data can never be reinterpreted as code no matter what it contains. In PHP that means PDO or mysqli with bound parameters:

// PDO, with named parameters
$stmt = $pdo->prepare('SELECT id, fname, sname, priv FROM users WHERE email = :email AND active = :active');
$stmt->execute([':email' => $email, ':active' => 'y']);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// mysqli, with positional parameters
$stmt = $mysqli->prepare('INSERT INTO pages (title, body, author_id) VALUES (?, ?, ?)');
$stmt->bind_param('ssi', $title, $body, $authorId);
$stmt->execute();

PDO or mysqli? The PHP documentation does not pick a winner; it says both are recommended for new projects and that performance is roughly equivalent. PDO is database-agnostic, so the same code works against MySQL, PostgreSQL or SQLite with a different connection string, and its named parameters read better in a long query. mysqli is MySQL-specific and exposes MySQL-only features. The argument that actually matters is not between them: it is between bound parameters and string concatenation. mysqli used properly is fine; mysqli used the way older material uses it, with variables interpolated into a query string, is the vulnerability. Note also that the original mysql_* functions, which appear in a great deal of surviving tutorial material, were removed from the language in PHP 7.0. Code using them does not run on any supported version.

Two supporting controls. Some parts of a query cannot be parameterised: table names, column names, and sort direction. For those, validate against an allow-list of permitted values rather than passing the user's string through. And the database account the application connects with should have only the privileges it needs; an application that never alters the schema does not need a connection with DROP TABLE rights, and limiting it means a successful injection achieves much less.

One pattern from the delivered material worth correcting explicitly, because it is the sort of thing that gets copied forward: storing the database connection handle in the session ($_SESSION['db']). Sessions are serialised to disk between requests and a connection resource is not meaningfully serialisable; the pattern also entangles the database layer with the session layer for no benefit. Open the connection in a configuration include, hold it in a variable or a small class, and let it close at the end of the request.

Generating the page, and escaping on the way out

The third element is producing the web documents, which means generating HTML from data. The security concern here is the mirror image of the last section: there, untrusted data was being read as code by the database; here, untrusted data is read as code by the browser.

Cross-site scripting happens when an application places user-supplied input into a page without neutralising it, so that input runs as script in the next visitor's browser, where it can read their session cookie, act as them, or rewrite what they see. Three forms: reflected, where the payload is in the request and bounced straight back; stored, where it is saved by the application and served to every later visitor, which is the most damaging in a content system; and DOM-based, where the flaw is in the page's own client-side JavaScript.

The defence is contextual output encoding. OWASP's framing in its Cross Site Scripting Prevention Cheat Sheet is important and often lost: there is no single escaping function, because the correct encoding depends on where in the page the value lands. HTML entity encoding for the body of a document, attribute encoding inside a quoted attribute, JavaScript escaping inside a script block, hex encoding in a CSS value, percent encoding in a URL parameter. Using the wrong one can introduce a weakness or break the output.

In PHP the HTML-context tool is htmlspecialchars():

echo '<p>Welcome back, ' . htmlspecialchars($user['fname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';

PHP 8.1 changed the default flags to include ENT_QUOTES | ENT_SUBSTITUTE, so passing them explicitly is now belt and braces rather than essential; passing them anyway documents the intent and survives being copied into an older codebase. The more robust answer at any scale above a few pages is a templating engine that escapes by default, Twig being the usual PHP choice and Blade the Laravel one, because a default that is safe is better than a discipline that has to be remembered on every line.

Escape on output, not on input. This is the rule that gets inverted most often. Sanitising data as it arrives and storing the sanitised version means the stored data is now wrong for every other context (an email, a PDF, an API response), and means double-escaping artefacts appear the moment anything is edited. Store what the user actually typed, validated for shape and length; escape it appropriately each time it is rendered.

Validation is a separate job from escaping, and both are needed. Validation asks whether the value is acceptable: is this a well-formed email address, is this a positive integer within range, is this one of the three permitted statuses, is this under the length limit? Reject what fails, with a message that tells the user what to fix. The unit's own emphasis on validating every field is right, and the list is worth keeping: empty when required, wrong type, negative where only positive makes sense, above the maximum the storage can hold (a signed 32-bit integer stops at 2,147,483,647), text where a number is expected, and out of the range the business rule allows.

Statelessness and session management

The knowledge evidence names stateless programming and session management together, and they are the same topic seen from two sides.

HTTP is stateless: each request stands alone, carrying no memory of the one before it. The server, having sent a response, retains nothing that links the next request to the last. That is a deliberate design choice and it is why the web scales, but it means an application that needs to know who you are has to arrange that for itself.

The arrangement is a session. On login, the server creates a session record on its own side, holding whatever it needs to know about you, and issues an identifier. The browser stores that identifier in a cookie and sends it back with every subsequent request, and the server uses it to find the record again. The identifier is therefore equivalent to your password for the life of the session, which is the fact that makes everything below necessary.

In PHP, session_start() begins or resumes the session and $_SESSION is the array that persists across requests. The important configuration is in php.ini or set at runtime, and php.net documents it under session security:

Session settings that matter

session.use_strict_mode=On makes PHP accept only session identifiers it generated. It is off by default, which means an attacker can otherwise set a victim's session identifier to one they already know and wait for the victim to log in with it. This is session fixation, and strict mode is its principal defence.

session.cookie_httponly=On stops client-side JavaScript from reading the cookie through document.cookie, so a cross-site scripting flaw cannot simply lift the session identifier. It does nothing against cross-site request forgery.

session.cookie_secure=On sends the cookie only over HTTPS, so it cannot be captured from an unencrypted request.

session.cookie_samesite="Lax" or "Strict" limits when the browser attaches the cookie to cross-site requests, which is defence in depth against cross-site request forgery. This setting was added in PHP 7.3, which is why older material does not mention it at all.

session.use_only_cookies=On and session.use_trans_sid=Off keep the identifier out of URLs, where it would end up in browser history, server logs and referrer headers. The transparent session ID settings are deprecated as of PHP 8.4.

Regenerate the identifier at every privilege change. session_regenerate_id(true) immediately after a successful login, and again on any change of role or password. OWASP's Session Management Cheat Sheet is unambiguous that this is mandatory rather than optional, because it is what defeats session fixation: even if an attacker planted an identifier, it is discarded the moment the user actually authenticates.

Set timeouts, and enforce them on the server. An idle timeout ends a session that has not been used for a period; an absolute timeout ends it regardless of activity. OWASP's indicative figures are 2 to 5 minutes idle for high-value applications and 15 to 30 minutes for low-risk ones, with an absolute timeout of 4 to 8 hours. A client-side timer is a convenience; the server decides.

Destroy the session properly on logout. Clear $_SESSION, call session_destroy(), and expire the cookie. A logout that only redirects leaves a valid session behind.

Authentication, and the part of this unit that is now simply wrong

The performance evidence asks for a script that manages sessions and creates secure logins, and the knowledge evidence names authentication and web security. This is where the delivered material has to be corrected rather than updated, so it is worth being exact about what changed and why.

The pattern in the older material is to hash a password with MD5 and then hash the result with SHA1. Two separate problems with that, and they are worth separating because the double-hash shows a misunderstanding of both.

The first is that MD5 and SHA1 are broken as collision-resistant hashes. NIST formally retired SHA-1 on 15 December 2022, giving federal agencies until the end of 2030 to stop using it, on the grounds that collision attacks are practical; MD5 fell considerably earlier.

The second, and the one that actually matters here, is that MD5 and SHA1 are fast general-purpose hashes, and speed is precisely the wrong property for a password hash. They were designed to digest large files quickly, which means an attacker holding a stolen database can test billions of candidate passwords per second on commodity graphics hardware. Chaining two fast hashes does not slow anything down; two fast operations are still fast. And because neither step adds a salt, identical passwords still produce identical stored values, so an attacker who cracks one account has cracked every account using that password, and precomputed tables still apply. The double hash looks like extra effort and provides essentially none.

What to do instead, in PHP, is a three-function API and nothing else.

// Storing a password
$hash = password_hash($plaintext, PASSWORD_DEFAULT);
// ... store $hash in a VARCHAR(255) column

// Checking a password
if (password_verify($plaintext, $storedHash)) {
    // authenticated
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($plaintext, PASSWORD_DEFAULT);
        // ... update the stored hash
    }
    session_regenerate_id(true);
}

password_hash() generates a cryptographically secure salt automatically and stores it inside the returned string, so the salt is never something the programmer handles or gets wrong. password_verify() extracts the algorithm, cost and salt from the stored string and does a timing-safe comparison. password_needs_rehash() is the piece people miss and the reason the column must be generous: it lets you migrate every stored hash to a stronger algorithm transparently, one user at a time, as each next logs in.

PASSWORD_DEFAULT still maps to bcrypt in PHP 8.x, and PHP 8.4 raised the default bcrypt cost from 10 to 12. The documentation warns that the default is designed to change over time as stronger algorithms are added, which is exactly why the storage column should be at least 255 bytes rather than the 60 that bcrypt currently needs. PASSWORD_ARGON2ID can be selected explicitly where the interpreter was built with libargon2 support, which is not universal on shared hosting.

What current guidance prefers. OWASP's Password Storage Cheat Sheet ranks Argon2id first, scrypt second where Argon2id is unavailable, bcrypt for legacy systems, and PBKDF2 where FIPS-140 compliance is required, with minimum parameters given for each: Argon2id at 19 MiB of memory, 2 iterations and 1 degree of parallelism; bcrypt at a work factor of at least 10, with its hard 72-byte input limit noted; PBKDF2 at 600,000 iterations with HMAC-SHA-256. So the honest teaching position is that PHP's default is acceptable and is what most hosting will give you, that Argon2id is preferable where the build supports it, and that the gap between either of those and MD5-then-SHA1 is not a matter of degree.

The password rules themselves have changed, and this catches almost everyone. NIST SP 800-63B-4, published 31 July 2025, superseding the March 2020 edition, sets a minimum length of 15 characters where a password is the single authentication factor and 8 where it is one factor within multi-factor authentication; requires that verifiers permit at least 64 characters; states that verifiers and credential service providers shall not impose composition rules such as requiring a mixture of character types; and states that they shall not require subscribers to change passwords periodically, forcing a change only on evidence of compromise. It also requires prospective passwords to be checked against a blocklist of common, expected and compromised values, and prohibits password hints retrievable by an unauthenticated party. So a login form that demands eight characters with an uppercase letter, a number and a symbol, and forces a change every ninety days, is wrong on three counts against the current standard, and the reasoning is behavioural: every one of those rules pushes people towards predictable patterns and password reuse.

Beyond passwords. OWASP's current Multifactor Authentication Cheat Sheet recommends phishing-resistant authenticators, describing FIDO2 and WebAuthn as the most secure form of multi-factor authentication because they bind the authentication to the legitimate origin and so resist credential theft, prompt fatigue and reverse-proxy phishing. It explicitly downgrades codes delivered by SMS, noting that SP 800-63B-4 designates SMS and telephone-delivered codes as a restricted authenticator because of SS7 interception, SIM swapping and number porting. The standard behind passkeys reached a milestone recently: Web Authentication Level 3 became a W3C Recommendation on 25 August 2026. Adoption figures come mostly from the FIDO Alliance, an industry body reporting on its own standard, so treat them as promotional even where the survey method is sound; its World Passkey Day release of 7 May 2026 reports roughly 5 billion passkeys in use and 68 per cent of surveyed organisations having deployed or actively deploying them for employee sign-in.

For a unit at this level the practical takeaway is not to implement WebAuthn from scratch. It is to know that a password-only login is now the weakest acceptable design, that adding a second factor is the single highest-value improvement available, and that the direction of travel is away from passwords entirely.

Authorisation: the check that is separate from the login

Authentication answers "who are you". Authorisation answers "are you allowed to do this". They are different questions and conflating them is the most common serious flaw in a first content management system.

The failure mode is specific. A developer builds a privilege system, hides the administrative links from ordinary users, and stops there. The links are hidden; the pages are not protected. An ordinary user who types the administrative URL directly reaches it, because nothing on the server ever checked. Related is the insecure direct object reference: a page that loads a record from a value in the URL, such as edit.php?id=7, without checking that the logged-in user is entitled to record 7, so changing 7 to 8 opens someone else's record. Neither needs any tooling to exploit. A browser and a guess are enough.

Broken access control has been the number one entry in the OWASP Top 10 in both the 2021 and 2025 editions, and this is why.

The rule is that every request that reaches protected data performs its own authorisation check, on the server, against the session's identity, regardless of what the interface offered. Not the page that renders the link; the page that does the work. In a privilege-tiered system the check has a shape worth internalising:

// At the top of every page that does something privileged
require_login();                       // is there a valid session at all?
require_privilege(PRIV_ADMIN);         // does this user hold the required level?
$target = load_user($_GET['id']);      // fetch the target record
require_can_manage($_SESSION['priv'], $target['priv']);  // may this user act on THIS record?

That last line is the one most designs omit. A tiered system where an administrator can manage users below their own level needs the comparison between the actor's level and the target's level, not merely a check that the actor is an administrator. Without it, one administrator can edit or delete another, or elevate themselves, and a system with a top administrator tier can be taken over from the tier below it.

Two further habits. Deny by default, so that a page without an explicit check fails closed rather than open; putting the check in a shared include that every protected page requires is how that is achieved in a small codebase. And never trust a privilege level that arrived from the browser: the user's role comes from the server-side session or is re-read from the database, never from a hidden form field, a cookie value or a URL parameter.

Uploading files, and why images are dangerous

The performance evidence asks for a script that uploads and retrieves images, which sounds like the most innocuous requirement in the unit and is one of the most dangerous.

An upload lets an anonymous or semi-trusted party place a file of their choosing onto your server. If that file can then be executed, they have remote code execution; if it can be served back with the wrong content type, they have stored cross-site scripting; if it is large enough or numerous enough, they have a denial of service. The three failures that appear in almost every first implementation are trusting $_FILES['file']['type'], which is simply a header the client supplied and can say anything; keeping the user's filename, which permits double extensions, null bytes, path traversal and overwriting; and writing the file into a web-accessible /uploads/ directory, which is what makes execution possible.

The controls in OWASP's File Upload Cheat Sheet are worth following item by item:

Handling an upload safely

Allow-list the extensions rather than block-listing them, and watch for double extensions and embedded null bytes. A block-list will always be missing something.

Validate the type properly. Check the file signature, the first bytes that identify the format, and for images verify the file actually parses as an image. Never trust the Content-Type header, which can be spoofed.

Generate the stored filename yourself, a UUID or similar, and keep the user's original name only as a display label, length-limited and restricted to alphanumerics plus hyphen and full stop.

Store outside the web root, or on separate storage, and serve files through a script that maps an identifier to the stored file, applying authorisation on the way. Where files must live under the web root, configure the server so that directory cannot execute anything.

Enforce a size limit at both the application and the server, and a limit on how many uploads a user can make.

Re-encode images server-side. Reading the uploaded image and writing it out again destroys anything hidden inside it, including a payload appended to a valid image file. This is the single most effective control for image uploads specifically.

Apply authorisation and cross-site request forgery protection to the upload endpoint itself, and scan uploads where the environment allows it.

The unit also makes a design point in passing that is worth stating as a rule: store the image on the server's filesystem or object storage and keep only the reference in the database. Storing binary data in database rows bloats the database, slows every backup, and makes serving the file harder than it needs to be.

Errors, and what the public should never see

The fifth element asks you to determine and implement permissions to prevent error messages displaying to the public, and this is a genuinely important control rather than a cosmetic one.

An unhandled error in a web application can disclose the file path on disk, the framework and its version, the database structure implied by a failed query, and sometimes fragments of the query including data. Each of those is reconnaissance an attacker would otherwise have to work for, which is why error-based SQL injection is a named technique: the error message is the output channel.

The rule is that a production site shows the user a generic, friendly message and writes the detail to a log only the administrator can read. In PHP that is two settings pulling in opposite directions by environment:

; Production
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/app-error.log   ; outside the web root
error_reporting = E_ALL
; Development
display_errors = On
log_errors = On
error_reporting = E_ALL

Note that error_reporting stays at E_ALL in both. The distinction is not which errors are detected but where they are sent. A production site that suppresses errors rather than logging them has traded one problem for a worse one, because now nobody knows anything is broken.

The error_reporting() function sets the same level at runtime for the current script, taking a bitmask of constants: E_ERROR for fatal runtime errors that halt the script, E_WARNING for non-fatal runtime warnings, E_NOTICE for conditions that may or may not indicate a bug, E_DEPRECATED for features scheduled for removal, and E_ALL for everything. It can be combined, so E_ALL & ~E_NOTICE reports everything except notices. The related suppression operator @, which silences errors on a single expression, is worth knowing mainly so that you can recognise and remove it; it hides problems rather than handling them, and in PHP 8 it no longer suppresses the most serious conditions anyway.

The better pattern in PHP 8 is to let errors be exceptions and catch them where you can do something useful, presenting the user with a message that says what to do next and logging the detail with enough context to diagnose it: a timestamp, the request path, the user identifier if there is one, and a reference number you can also show the user so that a support call can be matched to a log entry.

One related habit: turn off anything that advertises the software. The expose_php setting, which adds PHP's version to the response headers, should be off, and the web server's own version banner should be suppressed. This is not a strong control, since a determined attacker will fingerprint the stack anyway, but it removes a free hint.

Serving the site over HTTPS

The performance evidence asks you to configure one web server to deliver the website using HTTPS, and the knowledge evidence names HTTP and HTTP Secure together. The short version is that HTTPS is HTTP carried inside a TLS-encrypted channel, so that the conversation cannot be read or altered in transit, and that it is no longer optional for anything.

Why it stopped being optional. Without it, every value in every request travels as readable text across every network between the user and the server: passwords, session cookies, form contents. Browsers now mark plain HTTP as not secure, some features are restricted to secure contexts, and search engines take it into account. A site that handles a login and serves it over HTTP has no meaningful security regardless of how the password is stored.

Which versions to serve. TLS 1.0 and 1.1 were formally deprecated by RFC 8996 in March 2021, on the grounds that their integrity and authentication depend on SHA-1, and the major browsers had already disabled them by default in 2020. Serve TLS 1.3 and TLS 1.2 and disable everything below. TLS 1.3 was originally RFC 8446 of August 2018; that document has since been obsoleted by RFC 9846 of July 2026, described in its own text as a minor update that retains the same version number and is backward compatible, so the protocol a technician configures has not changed even though the reference has.

Getting a certificate. Let's Encrypt issues free domain-validated certificates through the ACME protocol, and most hosting control panels and web servers can request and renew them automatically. This has removed cost as a reason not to use HTTPS.

The change worth planning for is certificate lifetime. The CA/Browser Forum passed ballot SC-081v3 on 11 April 2025, introducing a schedule that reduces the maximum validity of a TLS certificate from 398 days to 47 days between March 2026 and March 2029, with domain validation data reuse falling to 10 days over the same period. The first step has already taken effect, so as at September 2026 the ceiling is 200 days rather than 398; it drops to 100 days from March 2027 and to 47 days from March 2029. This is the detail most summaries get wrong, reporting "47 days" as though it applied now. Let's Encrypt has separately made six-day certificates and IP address certificates generally available since 15 January 2026, as an opt-in profile rather than a default, and has said it is gradually reducing its standard 90-day lifetime towards 45 days over coming years.

The consequence is simple and worth stating to any client: certificate renewal has to be automated. At 47 days, a manual renewal process is not an operating model, it is a scheduled outage.

Redirecting HTTP to HTTPS, and a correction to the usual advice. The delivered material teaches putting a rewrite rule in a .htaccess file, which works and is the right answer on shared hosting. It is not the recommended method where you control the server, and Apache's own documentation on .htaccess files is blunt about why. On .htaccess generally it says that if you have access to the main server configuration file you should put all of your configuration there instead, including mod_rewrite rules, for two reasons: performance, because Apache searches every parent directory for a .htaccess file on every request whether or not those files exist, and security, because you are permitting users to modify server configuration. On HTTPS redirection specifically, Apache's recommended recipe does not use mod_rewrite at all:

<VirtualHost *:80>
    ServerName www.example.com
    Redirect permanent "/" "https://www.example.com/"
</VirtualHost>
<VirtualHost *:443>
    ServerName www.example.com
    # TLS configuration here
</VirtualHost>

The rewrite version is the documented fallback for when you cannot reach the main configuration:

RewriteEngine On
RewriteCond "%{HTTPS}" !=on
RewriteRule "^(.*)" "https://%{SERVER_NAME}$1" [R=301,L]

So the honest framing is: virtual host configuration first, .htaccess when you do not own the server, and know which situation you are in. Most students will be on shared hosting, where the second is correct, and knowing that it is the fallback rather than the method is what distinguishes understanding from copying.

Finish the job with HSTS. A redirect still leaves one unencrypted request at the start. HTTP Strict Transport Security, defined in RFC 6797 of November 2012, is a response header that tells the browser to use HTTPS for this site for a stated period, so subsequent visits never make the plain request at all. Add includeSubDomains where every subdomain is served securely, and set a long max-age only once you are confident, because it is not easily reversible.

Hardening the server around the application

Performance criterion 5.2 asks you to configure server software to minimise potential database attacks, which in practice means a short list of settings and privileges around the application rather than anything exotic.

Keep the database off the internet. A small application's database should accept connections from the application host only, typically over localhost. A database listening on a public address is found by automated scanners within hours.

Use a dedicated database account with minimal privileges. The application's account needs SELECT, INSERT, UPDATE and DELETE on its own database and nothing else. It does not need DROP, CREATE, FILE, or access to other databases, and it should not be the root account. This is the control that turns a successful SQL injection from a catastrophe into a serious but bounded incident.

Keep credentials out of the web root and out of version control. A configuration file containing a database password should sit outside the directory the web server serves, or at minimum be a .php file that outputs nothing if requested directly, and it should never be committed to a repository. Environment variables are the current convention for exactly this reason.

Set file and directory permissions deliberately. Directories readable and traversable, files readable, nothing writable by the web server process except the specific directories that must be (an upload directory, a cache directory), and nothing that is writable also being executable.

Send the security response headers. These turn browser features into controls and cost nothing: Strict-Transport-Security as above; Content-Security-Policy, which tells the browser which sources of script and other content are permitted and is the strongest single defence against cross-site scripting because it stops an injected script running even if it reaches the page; X-Content-Type-Options: nosniff, which stops the browser second-guessing the declared content type; and frame controls, either X-Frame-Options or the CSP frame-ancestors directive, to defeat clickjacking. OWASP's recommended strict policy is script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; with a simpler fallback of default-src 'self'; frame-ancestors 'self'; form-action 'self'; where a strict policy is not achievable. Worth noting for accuracy: Content Security Policy Level 3 is still a W3C Working Draft, dated 13 August 2026, despite being universally deployed, so it is current practice rather than a finished standard. And OWASP is explicit that CSP is an additional layer rather than a substitute for output encoding.

Back up, and rehearse the restore. A database backup that has never been restored is an assumption. Automate it, store a copy somewhere the web server cannot reach so that a compromise of the application does not take the backups with it, and restore one at least once.

Testing and debugging

The fourth element is testing, and it asks for two things: test the document and rectify issues, and complete test results documentation for discussion and acceptance. The second half is the part that turns testing into evidence.

Write the test plan from the requirements. Each requirement produces at least one test with an expected result, so that passing is a fact rather than an impression. For a form, that means a row per field per condition: valid input accepted, empty rejected with a useful message, wrong type rejected, out-of-range rejected, boundary values handled (the maximum permitted value accepted, one more than the maximum rejected), and, importantly, a hostile input handled safely. Testing an apostrophe in a surname is a functional test and an injection test at the same time, which is a neat illustration of why validation and security are not separate activities.

Test the authorisation grid deliberately. Create an account at each privilege level and try, from each one, both what it should be able to do and what it should not, including reaching a privileged URL directly rather than through a hidden link. That direct-URL test is the one that catches the flaw described earlier, and it takes a minute.

Record the results. What was tested, what data was used, what was expected, what happened, the date, and what was changed in response. Documented test results are what the unit asks for and are also what lets someone else verify the work, including the version of you that comes back to this code in six months.

The tools, and what each is for. Browser developer tools show the request and response, the headers, the cookies, the console errors and the timings, and are the first place to look when something behaves differently in the browser than expected. Server and application error logs are where the server-side detail lands once display_errors is off. A step debugger, Xdebug being the PHP one, lets you pause execution and inspect variables, which is faster than scattering print statements and is a skill worth acquiring early. Automated testing frameworks, PHPUnit for PHP, turn tests into code that reruns on every change; that is more than this unit requires and is where professional practice sits. Validators check that the generated markup is well-formed. For security specifically, the distinction in the knowledge evidence between static and dynamic testing is worth knowing: static application security testing reads the source without running it and catches flawed code early, dynamic testing exercises the running application from the outside the way an attacker meets it, and the two are complementary rather than alternatives.

Debugging is a method. Reproduce it reliably first, because an intermittent fault you cannot trigger cannot be fixed or verified. Then narrow it: is the fault in the input, in the processing, or in the output? Print or inspect the value at each stage until you find the point where it stops being what you expect. Change one thing at a time. And when it is fixed, add the case that broke it to the test plan, so that it stays fixed.

Where this unit sits in the OWASP Top 10

The OWASP Top 10 is the industry's baseline list of web application risk categories, and a current edition was released in November 2025. Knowing it matters here because almost every security topic in this unit maps onto an entry, and because the numbering has changed in ways that mislead people who quote it without the year.

Code Category
A01:2025 Broken Access Control
A02:2025 Security Misconfiguration
A03:2025 Software Supply Chain Failures
A04:2025 Cryptographic Failures
A05:2025 Injection
A06:2025 Insecure Design
A07:2025 Authentication Failures
A08:2025 Software or Data Integrity Failures
A09:2025 Security Logging and Alerting Failures
A10:2025 Mishandling of Exceptional Conditions

Three changes from the 2021 list are worth knowing. Software Supply Chain Failures is new as a category and enters at third, expanded well beyond the old "Vulnerable and Outdated Components", which reflects the fact that most applications are mostly other people's code. Mishandling of Exceptional Conditions is entirely new at tenth, which is directly relevant to this unit's error-handling element. And server-side request forgery has gone as its own entry, folded into Broken Access Control.

The change most likely to be misread is that Injection fell from third to fifth. That does not mean SQL injection stopped mattering. The list is ranked on how often categories appear in the contributed data, and injection falling means other classes appeared more often, not that this one was solved. Parameterised queries are still the defence and the consequence of omitting them is unchanged. Whenever you cite the Top 10, name the year, because the list has been renumbered repeatedly and a claim about "A03" means different things in 2021 and 2025.

Mapping this unit onto it: the authorisation section is A01; the server hardening and error-message sections are A02 and A10; the database section is A05; the password storage section is A04 and A07; the session section is A07; and the dependency section below is A03.

Dependencies, and the supply chain you inherit

Almost nothing is written from scratch any more. A PHP application pulls libraries through Composer, a JavaScript build pulls them through npm, and a moderately sized project has hundreds of packages it never chose directly, each of which runs with the same privileges as the code that included it. This is why supply chain failures entered the Top 10 at third, and it is the topic most absent from material written before about 2023.

The practical discipline at the scale of this unit:

Keep a lock file and commit it, so that the exact versions installed are reproducible rather than whatever was latest on the day. Audit dependencies regularly, which for Composer is composer audit and for npm is npm audit, both of which check installed versions against published advisory databases. Update deliberately and on a schedule rather than never, because an unmaintained dependency tree is the most common way an otherwise well-written application becomes vulnerable. Prefer fewer, well-maintained packages over many small ones, and look at a package's release history and issue tracker before adopting it. And remember that a dependency's dependencies are yours too.

AI-assisted coding, and what it changes about this work

Writing server-side code in 2026 usually means writing it alongside a model that suggests the next few lines. That is the working reality and pretending otherwise would date this page immediately. What matters for a security-focused unit is being precise about what the evidence actually supports, because popular coverage conflates several different claims.

The security claim is reasonably well evidenced, and it lands directly on this unit's subject matter. Veracode's 2026 GenAI Code Security Report, published 28 July 2026, found an average security pass rate of 56 per cent across the models tested, effectively flat against 55 per cent the year before, meaning roughly 44 per cent of generation tasks introduced a risky vulnerability. The per-category figures reported alongside it are the interesting part: SQL injection 83 per cent pass, cryptographic algorithm selection 87 per cent, cross-site scripting 15 per cent, log injection 12 per cent. The framing the report uses is that syntax is solved and security is not; models produce syntactically correct code nearly all the time while security performance has not improved. Two caveats belong with the figure: Veracode sells application security testing, so it has a commercial interest in the result, and the measure is whether a model produced vulnerable code for a task designed to invite a vulnerability, which is not the same as how much vulnerable code reaches production.

The 15 per cent figure for cross-site scripting is worth sitting with, because output escaping is precisely what this unit teaches. Generated code is comparatively good at reaching for a prepared statement and comparatively poor at escaping what it prints. So the review habit follows directly: when reading generated server-side code, check the output path first.

The supply chain claim is well evidenced and specific. A paper presented at the 34th USENIX Security Symposium in August 2025, analysing 576,000 generated code samples across two languages and 16 models, found that commercial models hallucinated package names at a rate of at least 5.2 per cent and open-source models at 21.7 per cent, producing over 205,000 unique non-existent package names. The attack that follows has been named slopsquatting: an attacker registers a package name that models repeatedly invent, so a developer who runs an AI-suggested composer require or npm install line installs attacker code. The hallucination rates are the measured part; the volume of successful real-world attacks is much less well established and is reported mostly in vendor material, so it is worth keeping those two claims separate. The practical rule is small and absolute: verify that a suggested package exists, and is the one you meant, before installing it.

The productivity claim is genuinely unsettled, and it is worth saying so rather than repeating either enthusiasm or scepticism. METR's randomised controlled trial published 10 July 2025 found that 16 experienced open-source developers working on 246 real issues in their own repositories took 19 per cent longer with AI tools than without, while believing afterwards that they had been faster. The authors are unusually careful about the limits: 16 developers, limited prior experience with the tool, unusually high code quality standards, and a snapshot of a fast-moving capability; they noted in an update in February 2026 that the results no longer reflect the current impact of newer models. The honest position is that the security finding is better supported than the productivity finding, and that the two are frequently conflated in coverage of both.

Guidance from reputable bodies exists but is narrower than it looks. NIST SP 800-218A, finalised in July 2024, extends the Secure Software Development Framework with AI-specific practices, but it is about developing AI systems securely rather than about reviewing AI-generated application code, and should not be cited as though it were. ASD's ACSC guidance "Engaging with Artificial Intelligence", last updated 24 January 2024, recommends applying baseline mitigations to AI systems, scrutinising the supply chain, and training staff on the extent to which outputs can be relied on. Joint guidance on the careful adoption of agentic AI services was released on 30 April 2026 by NSA with ASD's ACSC and partner agencies. None of these is a checklist for reviewing a generated function, which means the professional judgement is still yours.

What that judgement amounts to, for this unit, is a short list. Read every line you did not write before you run it. Check the output escaping. Check that queries are parameterised rather than concatenated. Check that the package exists. Check that an authorisation check is present and not merely a hidden link. And never submit, ship, or sign off on code you cannot explain, because the person who has to fix it at two in the morning is going to be you.

Sources used

The unit's application, elements, performance criteria and foundation skills are from the training.gov.au unit descriptor for ICTWEB430 (Release 1, ICT Training Package Version 4.0), with the performance evidence, knowledge evidence and assessment conditions from the companion assessment requirements document, both read from the copies held in the unit folder; nominal hours are from the Victorian purchasing guide for the ICT training package. The markup history is from the W3C's own records: the XHTML 1.0 Recommendation of 26 January 2000 (second edition 1 August 2002) and XHTML 1.1 of 23 November 2010, both marked Superseded Recommendation on 27 March 2018; the XHTML2 FAQ of 2 July 2009; the W3C and WHATWG agreement announced 28 May 2019; and the W3C's endorsement of the WHATWG Review Draft on 28 January 2021. The current markup standard is the WHATWG HTML Living Standard. PHP version support, end-of-life dates and function behaviour are from php.net's supported versions and end-of-life pages and the manual entries for password_hash, session security, and the mysqli and PDO comparison, read 14 September 2026; the PHP 8.0 and 8.5 release notes are from the same source. Database, escaping, session, authentication, upload and Content Security Policy practice is from the OWASP Cheat Sheet Series, specifically the SQL Injection Prevention, Password Storage, Session Management, Cross Site Scripting Prevention, Multifactor Authentication, File Upload and Content Security Policy cheat sheets; the series carries no per-page publication dates. NIST material is SP 800-63B-4, published 31 July 2025, and the SHA-1 retirement announcement of 15 December 2022. Transport security is RFC 8996 of March 2021 for the TLS 1.0 and 1.1 deprecation, RFC 8446 of August 2018 for TLS 1.3 as obsoleted by RFC 9846 of July 2026, and RFC 6797 of November 2012 for HSTS; certificate lifetimes are from the CA/Browser Forum's ballot SC-081v3 announcement of 11 April 2025, with the phased schedule corroborated by DigiCert (16 May 2025) and SSL.com (last modified 17 April 2026), and Let's Encrypt's announcements of 9 January 2025 and 15 January 2026. Apache guidance on .htaccess and HTTPS redirection is from the Apache HTTP Server 2.4 documentation. WebAuthn Level 3 became a W3C Recommendation on 25 August 2026; passkey adoption figures are from the FIDO Alliance's World Passkey Day release of 7 May 2026 and are the industry body's own survey. The OWASP Top 10 2025 entries are taken from OWASP's own project site rather than from secondary coverage, following the caution recorded on the VU23222 page that news summaries have been inconsistent about ordering; the 2021 list is from top10.owasp.org. Language landscape figures are from W3Techs' server-side programming language survey dated 13 September 2026 and the Stack Overflow Developer Survey 2025. AI-assisted coding evidence is Veracode's 2026 GenAI Code Security Report of 28 July 2026, the USENIX Security 2025 paper on package hallucinations by Spracklen and colleagues, and METR's randomised controlled trial of 10 July 2025 with its February 2026 update; the guidance cited is NIST SP 800-218A (July 2024), ASD's "Engaging with Artificial Intelligence" (last updated 24 January 2024), and the joint agentic AI guidance released 30 April 2026.